file_properties.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title locale="title/title">File Properties</title>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
  7. <link rel="stylesheet" href="../../script/semantic/semantic.min.css">
  8. <script type="text/javascript" src="../../script/jquery.min.js"></script>
  9. <script type="text/javascript" src="../../script/semantic/semantic.min.js"></script>
  10. <script type="text/javascript" src="../../script/ao_module.js"></script>
  11. <script type="text/javascript" src="../../script/applocale.js"></script>
  12. <link rel="stylesheet" href="../../script/ao.css">
  13. <style>
  14. body {
  15. overflow: hidden;
  16. }
  17. #filePropertiesWindow {
  18. background-color: var(--body_background);
  19. color: var(--body_text);
  20. }
  21. #filePropertiesWindow td,
  22. .header,
  23. p,
  24. div {
  25. color: var(--body_text) !important;
  26. }
  27. #filePropertiesWindow input {
  28. background-color: var(--body_background_secondary) !important;
  29. color: var(--text_color) !important;
  30. font-size: 1.2em;
  31. }
  32. .hidden {
  33. display: none !important;
  34. }
  35. .vertical-margin {
  36. margin-top: 4px !important;
  37. }
  38. .small.basic.white.fluid.button {
  39. color: var(--text_color_secondary) !important;
  40. }
  41. .small.basic.white.fluid.button:hover {
  42. background-color: var(--body_background_active) !important;
  43. }
  44. </style>
  45. </head>
  46. <body id="filePropertiesWindow">
  47. <br>
  48. <div class="ui container">
  49. <h3 class="ui header">
  50. <span locale="title/title">File Properties</span>
  51. <div class="sub header" locale="title/desc">Basic File Information</div>
  52. </h3>
  53. <div class="ui divider"></div>
  54. <div id="properties">
  55. </div>
  56. <br>
  57. <button class="ui small white basic fluid button singleFileOnly hidden" onclick="changeDefaultWebApp();"><i
  58. class="ui external square alternate icon blue"></i><span locale="button/changeDefault">Change Default
  59. WebApp</span></button>
  60. <button class="ui small basic white fluid button singleFileOnly hidden vertical-margin"
  61. onclick="viewVersionHistory();"><i class="ui undo green icon"></i><span locale="button/versionHistory">View
  62. Version History</span></button>
  63. <button class="ui small basic white fluid button linuxonly vertical-margin" onclick="openFilePermissionPanel();"
  64. locale="button/changeFilePermission">Change File Permissions</button>
  65. <br>
  66. </div>
  67. <div id="filesizeLoader" class="ui active dimmer">
  68. <div class="ui indeterminate text loader" locale="loader/loadingFileSize">Calculating File Size</div>
  69. </div>
  70. <script>
  71. //Initiate the view model
  72. var files = ao_module_loadInputFiles();
  73. var fileProperties = [];
  74. var fileInfo = {};
  75. function initFileProperties() {
  76. $("#properties").html("");
  77. if (files.length == 1) {
  78. //There are only 1 file to be shown
  79. getFileProp(files[0], renderSingleObject);
  80. } else if (files.length > 1) {
  81. for (var i = 0; i < files.length; i++) {
  82. getFileProp(files[i], function (data) {
  83. fileProperties.push(data);
  84. if (fileProperties.length == files.length) {
  85. renderMultipleObjects();
  86. }
  87. });
  88. }
  89. }
  90. }
  91. applocale.init("../locale/file_properties.json", function () {
  92. applocale.translate();
  93. initFileProperties();
  94. });
  95. //Hide windows / linux only operations
  96. $.get("/system/info/getArOZInfo", function (data) {
  97. if (data.HostOS == "windows") {
  98. $(".linuxonly").hide();
  99. } else {
  100. $(".windowsonly").hide();
  101. }
  102. });
  103. function viewVersionHistory() {
  104. var hashPassthrough = encodeURIComponent(JSON.stringify(files));
  105. ao_module_newfw({
  106. url: "SystemAO/file_system/file_versions.html#" + hashPassthrough,
  107. width: 570,
  108. height: 480,
  109. appicon: "SystemAO/file_system/img/properties.png",
  110. title: "File Version History",
  111. });
  112. }
  113. function getFileProp(vpath, callback) {
  114. $.ajax({
  115. url: "../../system/file_system/getProperties",
  116. data: { path: vpath },
  117. method: "POST",
  118. success: function (data) {
  119. callback(data);
  120. fileInfo = data;
  121. //Initialize system theme
  122. fpw_loadPreference("file_explorer/theme", function (data) {
  123. if (data.error === undefined) {
  124. if (data == "darkTheme") {
  125. $("body").addClass("darkTheme");
  126. } else {
  127. $("body").addClass("whiteTheme");
  128. }
  129. }
  130. });
  131. }
  132. })
  133. }
  134. function openFilePermissionPanel() {
  135. var hashPassthrough = encodeURIComponent(JSON.stringify(files));
  136. ao_module_newfw({
  137. url: "SystemAO/file_system/file_permission.html#" + hashPassthrough,
  138. width: 340,
  139. height: 480,
  140. appicon: "SystemAO/file_system/img/properties.png",
  141. title: "File Permissions",
  142. });
  143. }
  144. function renderMultipleObjects() {
  145. hideLoader();
  146. const filesizeSum = sumProperties(fileProperties, "Filesize");
  147. let filecount = 0, foldercount = 0;
  148. // Pre-build HTML
  149. let html = ui_getInput(fileProperties[0].VirtualDirname + "/", "Root Name");
  150. // Get counts
  151. fileProperties.forEach(item => {
  152. item.IsDirectory ? foldercount++ : filecount++;
  153. });
  154. // Add summary table
  155. html += ui_getText(applocale.getString("selection/multi", "Multiple selections"));
  156. html += buildCounterElements(filecount, foldercount);
  157. html += buildSummaryTable(filesizeSum);
  158. // Inject DOM once
  159. $("#properties").html(html);
  160. }
  161. // Build counter elements
  162. function buildCounterElements(fileCount, folderCount) {
  163. return [
  164. ui_getText(`${fileCount} ${applocale.getString("counter/files", "Files")}`),
  165. ui_getText(`${folderCount} ${applocale.getString("counter/folders", "Folders")}`)
  166. ].join("");
  167. }
  168. // Build summary table
  169. function buildSummaryTable(filesizeSum) {
  170. const totalSizeText = formatSize(filesizeSum);
  171. return ui_getTable([], [
  172. ["Virtual Directory", fileProperties[0].VirtualDirname + "/"],
  173. ["Total Size", totalSizeText]
  174. ]);
  175. }
  176. function sumProperties(data, propName) {
  177. var sum = 0;
  178. for (var i = 0; i < data.length; i++) {
  179. sum += data[i][propName];
  180. }
  181. return sum;
  182. }
  183. //Render one object property to the ui element
  184. function renderSingleObject(data) {
  185. hideLoader();
  186. let html = ''; // Pre-build html
  187. if (data.error !== undefined) {
  188. //Something went wrong
  189. html = `<h4 class="ui header">
  190. <i class="question icon"></i>
  191. <div class="content">
  192. File Properties Unknown
  193. <div class="sub header">The system were unable to read the selected file properties.</div>
  194. </div>
  195. </h4>
  196. <div class="ui divider"></div>
  197. <small>${data.error}</small>`;
  198. } else {
  199. // Info
  200. var filesizeText = "File Size";
  201. const isDir = data.IsDirectory;
  202. html += ui_getInput(data.Basename, isDir ? "Folder Name" : "File Name");
  203. html += ui_getText(data.MimeType);;
  204. if (!isDir && data.Basename.split(".").pop() !== "shortcut") {
  205. // Async content placeholder
  206. html += '<div id="asyncContent"></div>';
  207. } else {
  208. // Sync content
  209. html += buildFileTable(data, isDir);
  210. }
  211. }
  212. // Add DOM once
  213. $("#properties").html(html);
  214. // Async content
  215. if (!data.IsDirectory && data.Basename.split(".").pop() !== "shortcut") {
  216. loadAsyncContent(data);
  217. }
  218. }
  219. // Build file table
  220. function buildFileTable(data, isDir) {
  221. let sizeText = formatSize(data.Filesize);
  222. let lastModText = generateDisplayLastModTime(data.LastModTime);
  223. return ui_getTable([], [
  224. ["Virtual Path", data.VirtualPath],
  225. [isDir ? "Folder Size" : "File Size", sizeText],
  226. ["Permission", data.Permission],
  227. ["Last Modified", lastModText],
  228. ["File Type", isDir ? "Folder" : "File"],
  229. ["Owner", data.Owner]
  230. ]);
  231. }
  232. function loadAsyncContent(data) {
  233. $(".singleFileOnly").show();
  234. $.ajax({
  235. url: "../../system/modules/getDefault",
  236. method: "GET",
  237. data: {
  238. opr: "launch",
  239. ext: "." + data.Basename.split(".").pop(),
  240. mode: "launch"
  241. },
  242. success: function (openerinfo) {
  243. const content = buildFileTable(data, openerinfo);
  244. $("#asyncContent").replaceWith(content);
  245. }
  246. });
  247. }
  248. // Format bytes to human readable
  249. function formatSize(bytes) {
  250. if (bytes < 0) {
  251. return `<i class="times circle outline yellow icon"></i> ${applocale.getString(
  252. "properties/error/Not available for network folders",
  253. "Not available for network folders"
  254. )}`;
  255. }
  256. return bytesToSize(bytes) + ` (${bytes} bytes)`;
  257. }
  258. function hideLoader() {
  259. $("#filesizeLoader").hide();
  260. $("body").css('overflow-y', "auto");
  261. }
  262. //Model rendering scripts
  263. function ui_getInput(value, placeholder = "", type = "text") {
  264. return `<div class="ui fluid small input">
  265. <input type="${type}" placeholder="${placeholder}" value="${value}" readonly="true">
  266. </div>`
  267. }
  268. function ui_getText(value, color = "black") {
  269. return `<p style="color:${color}; margin-bottom:0px;">${value}</p>`;
  270. }
  271. function ui_getDivider() {
  272. return `<div class="ui divider"></div>`;
  273. }
  274. //head is a 1D array and table is 2D array
  275. function ui_getTable(heads, table) {
  276. // Cache the HTML parts
  277. const htmlParts = ['<table class="ui very basic fluid table">'];
  278. // Build thead
  279. if (heads.length > 0) {
  280. htmlParts.push(
  281. '<thead><tr>',
  282. heads.map(head => `<th>${head}</th>`).join(''),
  283. '</tr></thead>'
  284. );
  285. }
  286. // Build tbody
  287. htmlParts.push(
  288. '<tbody>',
  289. table.map(row =>
  290. `<tr>${row.map((cell, cellIndex) => {
  291. // Only translate the first column
  292. let content = cell;
  293. if (cellIndex === 0 && applocale) {
  294. const localeKey = `properties/key/${cell.trim()}`;
  295. content = applocale.getString(localeKey, cell);
  296. }
  297. return `<td style="word-break: break-all;">${content}</td>`;
  298. }).join('')
  299. }</tr>`
  300. ).join(''),
  301. '</tbody></table>'
  302. );
  303. return htmlParts.join('');
  304. }
  305. function bytesToSize(bytes) {
  306. var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
  307. if (bytes == 0) return '0 Byte';
  308. var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
  309. return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
  310. }
  311. /*
  312. Updates Oct 2020 - Matching File Explorer Theme on other file system tabs
  313. */
  314. function fpw_toggleDarkTheme() {
  315. $("#filePropertiesWindow").css({
  316. "background-color": "#242330",
  317. "color": "white",
  318. });
  319. $("#filePropertiesWindow td,.header,p,div").css({
  320. "color": "white",
  321. });
  322. $("#filePropertiesWindow .input").addClass("inverted transparent big")
  323. }
  324. function fpw_loadPreference(key, callback) {
  325. $.get("../../system/file_system/preference?key=" + key, function (data) {
  326. callback(data);
  327. });
  328. }
  329. /*
  330. Updates 30 Jan 2021: Added change of file opener
  331. */
  332. //Open Opener Selector for the given file
  333. function changeDefaultWebApp() {
  334. var ext = fileInfo.Ext;
  335. var openFileList = [];
  336. var openFileObject = {
  337. filepath: fileInfo.VirtualPath,
  338. filename: fileInfo.Basename,
  339. }
  340. openFileList.push(openFileObject);
  341. var openParamter = encodeURIComponent(JSON.stringify(openFileObject));
  342. ao_module_newfw({
  343. url: "SystemAO/file_system/defaultOpener.html#" + openParamter,
  344. width: 320,
  345. height: 510,
  346. appicon: "SystemAO/file_system/img/opener.png",
  347. title: "Default WebApp for " + ext,
  348. parent: ao_module_windowID,
  349. callback: "handleRefresh"
  350. });
  351. }
  352. function handleRefresh() {
  353. //Default opener changed. Update the display
  354. initFileProperties();
  355. }
  356. // Generate the display text for last modified time
  357. function generateDisplayLastModTime(lastModTime) {
  358. // Parse the date
  359. const parseDate = (str) => {
  360. const [datePart] = str.split(" ");
  361. const [year, month, day] = datePart.split("-");
  362. return new Date(year, month - 1, day); // Month index
  363. };
  364. const modTime = parseDate(lastModTime);
  365. const now = new Date();
  366. const [years, months, days] = calcDate(now, modTime);
  367. let displayText = "Unknown";
  368. if (years > 0) {
  369. if (years > 1 && applocale.getString("lastmod/time/s")) {
  370. displayText += applocale.getString("lastmod/time/s", "s");
  371. }
  372. displayText = `${years}${applocale.getString("lastmod/time/year", "year")}${applocale.getString("lastmod/time/ago")}`;
  373. } else if (months > 0) {
  374. if (months > 1 && applocale.getString("lastmod/time/s")) {
  375. displayText += applocale.getString("lastmod/time/s", "s");
  376. }
  377. displayText = `${months}${applocale.getString("lastmod/time/month", "month")}${applocale.getString("lastmod/time/ago")}`;
  378. } else if (days > 0) {
  379. if (days > 1 && applocale.getString("lastmod/time/s")) {
  380. displayText += applocale.getString("lastmod/time/s", "s");
  381. }
  382. displayText = `${days}${applocale.getString("lastmod/time/days", "day")}${applocale.getString("lastmod/time/ago")}`;
  383. } else {
  384. displayText = applocale.getString("lastmod/time/today", "Today");
  385. }
  386. return `${displayText} (${lastModTime})`;
  387. }
  388. function calcDate(endDate = new Date(), startDate) {
  389. // Make sure startDate is a Date object
  390. if (!(startDate instanceof Date)) {
  391. startDate = new Date(startDate);
  392. }
  393. let years = endDate.getFullYear() - startDate.getFullYear();
  394. let months = endDate.getMonth() - startDate.getMonth();
  395. let days = endDate.getDate() - startDate.getDate();
  396. // Cross months
  397. if (days < 0) {
  398. const lastMonth = new Date(endDate);
  399. lastMonth.setMonth(lastMonth.getMonth() - 1);
  400. days += new Date(
  401. lastMonth.getFullYear(),
  402. lastMonth.getMonth() + 1,
  403. 0
  404. ).getDate();
  405. months--;
  406. }
  407. // Cross years
  408. if (months < 0) {
  409. years--;
  410. months += 12;
  411. }
  412. return [years, months, days];
  413. }
  414. </script>
  415. </body>
  416. </html>