Skip to content

Commit 2470a64

Browse files
committed
Fix #2500: File Manager UI/UX improvements
This PR addresses multiple issues and feature requests from #2500: 1. Fix PR #2402 Markdown Parser regression (literal colon bug): - Changed all dialog templates from <div class="dfm_info"> to <span class="dfm_text"> - Markdown Extra parser can now handle inline elements without blank line - Removed obsolete <dt>/<dd> wrapper structure in Copy/Move templates - Cleaned up unused dfm_info CSS rules 2. Show total size of running transfer: - Enhanced file_manager rsync parser to calculate total transfer size - Averages 5 measurements at different progress points for ~2% accuracy - Displays 'Transferring X of Y' during operations 3. Show last N used destination paths in FileTree: - New PopularDestinations.php with frequency-based scoring system - Displays top 5 recent paths at FileTree root level - Automatic FUSE conflict prevention (/mnt/user vs /mnt/diskX) - Persistent storage in /boot/config/filemanager.json 4. Manually typing destination path updates FileTree: - New setupTargetNavigation() function - Navigate FileTree via arrow keys and Enter/Escape - Automatic FileTree open/close on input interaction - Prevents FileTree from closing when clicking inside dialog 5. Add 'Open Terminal here' button: - OpenTerminal.php now accepts path parameter via 'more' GET param - Creates custom startup script to set working directory - File Manager can open terminal at selected folder
1 parent 7399c2d commit 2470a64

10 files changed

Lines changed: 750 additions & 78 deletions

File tree

emhttp/plugins/dynamix/Browse.page

Lines changed: 366 additions & 7 deletions
Large diffs are not rendered by default.

emhttp/plugins/dynamix/include/Control.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<?
1414
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
1515
require_once "$docroot/webGui/include/Helpers.php";
16+
require_once "$docroot/plugins/dynamix/include/PopularDestinations.php";
1617

1718
// add translations
1819
$_SERVER['REQUEST_URI'] = '';
@@ -218,9 +219,19 @@ function quoted($name) {return is_array($name) ? implode(' ',array_map('escape',
218219
// add task to queue
219220
$data['task'] = rawurldecode($_POST['task']);
220221
file_put_contents($jobs, json_encode($data)."\n", FILE_APPEND);
222+
223+
// Update popular destinations for copy/move operations
224+
if (in_array($data['action'], ['3', '4', '8', '9']) && !empty($data['target'])) {
225+
updatePopularDestinations($data['target']);
226+
}
221227
} else {
222228
// start operation
223229
file_put_contents($active, json_encode($data));
230+
231+
// Update popular destinations for copy/move operations
232+
if (in_array($data['action'], ['3', '4', '8', '9']) && !empty($data['target'])) {
233+
updatePopularDestinations($data['target']);
234+
}
224235
}
225236
die();
226237
}

emhttp/plugins/dynamix/include/FileTree.php

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ function my_dir($name) {
5050

5151
$docroot = '/usr/local/emhttp';
5252
require_once "$docroot/webGui/include/Secure.php";
53+
require_once "$docroot/plugins/dynamix/include/PopularDestinations.php";
5354

5455
$mntdir = '/mnt/';
5556
$userdir = '/mnt/user/';
@@ -65,12 +66,49 @@ function my_dir($name) {
6566
$UDincluded = ['disks','remotes'];
6667

6768
echo "<ul class='jqueryFileTree'>";
68-
if ($_POST['show_parent'] == 'true' && is_top($rootdir)) {
69-
echo "<li class='directory collapsed'>$checkbox<a href='#' rel=\"".htmlspecialchars(dirname($rootdir))."\">..</a></li>";
69+
70+
// Show popular destinations at the top (only at root level and not in autocomplete mode)
71+
if (!$autocomplete && $rootdir === $root) {
72+
$popularPaths = getPopularDestinations(5);
73+
74+
// Filter popular paths to prevent FUSE conflicts between /mnt/user and /mnt/diskX
75+
if (!empty($popularPaths)) {
76+
$isUserContext = (strpos($root, '/mnt/user') === 0 || strpos($root, '/mnt/rootshare') === 0);
77+
78+
if ($isUserContext) {
79+
// In /mnt/user context: only show /mnt/user paths OR non-/mnt paths (external mounts)
80+
$popularPaths = array_filter($popularPaths, function($path) {
81+
return (strpos($path, '/mnt/user') === 0 || strpos($path, '/mnt/rootshare') === 0 || strpos($path, '/mnt/') !== 0);
82+
});
83+
} else if (strpos($root, '/mnt/') === 0) {
84+
// In /mnt/diskX or /mnt/cache context: exclude /mnt/user and /mnt/rootshare paths
85+
$popularPaths = array_filter($popularPaths, function($path) {
86+
return (strpos($path, '/mnt/user') !== 0 && strpos($path, '/mnt/rootshare') !== 0);
87+
});
88+
}
89+
// If root is not under /mnt/, no filtering needed
90+
}
91+
92+
if (!empty($popularPaths)) {
93+
echo "<li class='popular-header small-caps-label' style='list-style:none;padding:5px 0 5px 20px;'>Popular</li>";
94+
95+
foreach ($popularPaths as $path) {
96+
$pathName = basename($path);
97+
$htmlPath = htmlspecialchars($path);
98+
$htmlName = htmlspecialchars(mb_strlen($pathName) <= 33 ? $pathName : mb_substr($pathName, 0, 30).'...');
99+
// Use data-path instead of rel to prevent jQueryFileTree from handling these links
100+
// Use 'directory' class so jQueryFileTree CSS handles the icon
101+
echo "<li class='directory popular-destination' style='list-style:none;'>$checkbox<a href='#' data-path='$htmlPath'>$htmlName</a></li>";
102+
}
103+
104+
// Separator line
105+
echo "<li class='popular-separator' style='list-style:none;border-top:1px solid var(--inverse-border-color);margin:5px 0 5px 20px;'></li>";
106+
}
70107
}
71108

109+
// Read directory contents first (needed for both normal and autocomplete mode)
110+
$dirs = $files = [];
72111
if (is_dir($rootdir)) {
73-
$dirs = $files = [];
74112
$names = array_filter(scandir($rootdir, SCANDIR_SORT_NONE), 'no_dots');
75113
// add UD shares under /mnt/user
76114
foreach ($UDincluded as $name) {
@@ -89,25 +127,33 @@ function my_dir($name) {
89127
$files[] = $name;
90128
}
91129
}
92-
foreach ($dirs as $name) {
93-
// Exclude '.Recycle.Bin' from all shares and UD folders from '/mnt'
94-
if ($name === '.Recycle.Bin' || ($rootdir === $mntdir && in_array($name, $UDexcluded))) continue;
95-
$htmlRel = htmlspecialchars(my_dir($name).$name);
96-
$htmlName = htmlspecialchars(mb_strlen($name) <= 33 ? $name : mb_substr($name, 0, 30).'...');
97-
if (empty($match) || preg_match("/$match/", $rootdir.$name.'/')) {
98-
echo "<li class='directory collapsed'>$checkbox<a href='#' rel=\"$htmlRel/\">$htmlName</a></li>";
99-
}
130+
}
131+
132+
// Normal mode: show directory tree
133+
if ($_POST['show_parent'] == 'true' && is_top($rootdir)) {
134+
echo "<li class='directory collapsed'>$checkbox<a href='#' rel=\"".htmlspecialchars(dirname($rootdir))."\">..</a></li>";
135+
}
136+
137+
// Display directories and files (arrays already populated above)
138+
foreach ($dirs as $name) {
139+
// Exclude '.Recycle.Bin' from all shares and UD folders from '/mnt'
140+
if ($name === '.Recycle.Bin' || ($rootdir === $mntdir && in_array($name, $UDexcluded))) continue;
141+
$htmlRel = htmlspecialchars(my_dir($name).$name);
142+
$htmlName = htmlspecialchars(mb_strlen($name) <= 33 ? $name : mb_substr($name, 0, 30).'...');
143+
if (empty($match) || preg_match("/$match/", $rootdir.$name.'/')) {
144+
echo "<li class='directory collapsed'>$checkbox<a href='#' rel=\"$htmlRel/\">$htmlName</a></li>";
100145
}
101-
foreach ($files as $name) {
102-
$htmlRel = htmlspecialchars(my_dir($name).$name);
103-
$htmlName = htmlspecialchars($name);
104-
$ext = mb_strtolower(pathinfo($name, PATHINFO_EXTENSION));
105-
foreach ($filters as $filter) if (empty($filter) || $ext === $filter) {
106-
if (empty($match) || preg_match("/$match/", $name)) {
107-
echo "<li class='file ext_$ext'>$checkbox<a href='#' rel=\"$htmlRel\">$htmlName</a></li>";
108-
}
146+
}
147+
foreach ($files as $name) {
148+
$htmlRel = htmlspecialchars(my_dir($name).$name);
149+
$htmlName = htmlspecialchars($name);
150+
$ext = mb_strtolower(pathinfo($name, PATHINFO_EXTENSION));
151+
foreach ($filters as $filter) if (empty($filter) || $ext === $filter) {
152+
if (empty($match) || preg_match("/$match/", $name)) {
153+
echo "<li class='file ext_$ext'>$checkbox<a href='#' rel=\"$htmlRel\">$htmlName</a></li>";
109154
}
110155
}
111156
}
157+
112158
echo "</ul>";
113159
?>

emhttp/plugins/dynamix/include/OpenTerminal.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,43 @@ function command($path,$file) {
5151
// no child processes, restart ttyd to pick up possible font size change
5252
if ($retval != 0) exec("kill ".$ttyd_pid[0]);
5353
}
54-
if ($retval != 0) exec("ttyd-exec -i '$sock' '" . posix_getpwuid(0)['shell'] . "' --login");
54+
55+
$more = $_GET['more'] ?? '';
56+
if (!empty($more) && substr($more, 0, 1) === '/') {
57+
// Terminal at specific path - use 'more' parameter to pass path
58+
// Note: openTerminal(tag, name, more) in JS only has 3 params, so we reuse 'more'
59+
// Note: Used by File Manager to open terminal at specific folder
60+
61+
// Validate path
62+
$real_path = realpath($more);
63+
if ($real_path === false) {
64+
// Path doesn't exist - fall back to home directory
65+
$real_path = '/root';
66+
}
67+
68+
$name = unbundle($_GET['name']);
69+
$exec = "/var/tmp/$name.run.sh";
70+
$escaped_path = str_replace("'", "'\\''", $real_path);
71+
72+
// Create startup script similar to ~/.bashrc
73+
// Note: We can not use ~/.bashrc as it loads /etc/profile which does 'cd $HOME'
74+
$script_content = <<<BASH
75+
#!/bin/bash
76+
# Modify /etc/profile to replace 'cd \$HOME' with our target path
77+
sed 's#^cd \$HOME#cd '\''$escaped_path'\''#' /etc/profile > /tmp/$name.profile
78+
source /tmp/$name.profile
79+
source /root/.bash_profile 2>/dev/null
80+
rm /tmp/$name.profile
81+
exec bash --norc -i
82+
BASH;
83+
84+
file_put_contents($exec, $script_content);
85+
chmod($exec, 0755);
86+
exec("ttyd-exec -i '$sock' $exec");
87+
} else {
88+
// Standard login shell
89+
if ($retval != 0) exec("ttyd-exec -i '$sock' '" . posix_getpwuid(0)['shell'] . "' --login");
90+
}
5591
break;
5692
case 'syslog':
5793
// read syslog file
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?PHP
2+
/* Copyright 2005-2025, Lime Technology
3+
* Copyright 2012-2025, Bergware International.
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU General Public License version 2,
7+
* as published by the Free Software Foundation.
8+
*
9+
* The above copyright notice and this permission notice shall be included in
10+
* all copies or substantial portions of the Software.
11+
*/
12+
?>
13+
<?
14+
// Popular Destinations Management for File Manager
15+
// Uses frequency-based scoring with decay
16+
17+
define('POPULAR_DESTINATIONS_FILE', '/boot/config/filemanager.json');
18+
define('SCORE_INCREMENT', 10);
19+
define('SCORE_DECAY', 1);
20+
define('MAX_ENTRIES', 50);
21+
22+
/**
23+
* Load popular destinations from JSON file
24+
*/
25+
function loadPopularDestinations() {
26+
if (!file_exists(POPULAR_DESTINATIONS_FILE)) {
27+
return ['destinations' => []];
28+
}
29+
30+
$json = file_get_contents(POPULAR_DESTINATIONS_FILE);
31+
$data = json_decode($json, true);
32+
33+
if (!is_array($data) || !isset($data['destinations'])) {
34+
return ['destinations' => []];
35+
}
36+
37+
return $data;
38+
}
39+
40+
/**
41+
* Save popular destinations to JSON file
42+
*/
43+
function savePopularDestinations($data) {
44+
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
45+
file_put_contents(POPULAR_DESTINATIONS_FILE, $json);
46+
}
47+
48+
/**
49+
* Update popular destinations when a job is started
50+
* @param string $targetPath The destination path used in copy/move operation
51+
*/
52+
function updatePopularDestinations($targetPath) {
53+
// Skip empty paths or paths that are just /mnt or /boot
54+
if (empty($targetPath) || $targetPath == '/mnt' || $targetPath == '/boot') {
55+
return;
56+
}
57+
58+
// Normalize path (remove trailing slash)
59+
$targetPath = rtrim($targetPath, '/');
60+
61+
// Load current data
62+
$data = loadPopularDestinations();
63+
$destinations = $data['destinations'];
64+
65+
// Find target path first (before decay)
66+
$found = false;
67+
$targetIndex = -1;
68+
foreach ($destinations as $index => $dest) {
69+
if ($dest['path'] === $targetPath) {
70+
$found = true;
71+
$targetIndex = $index;
72+
break;
73+
}
74+
}
75+
76+
// Decay all scores by 1 (except the target path which we'll increment)
77+
foreach ($destinations as $index => &$dest) {
78+
if ($index !== $targetIndex) {
79+
$dest['score'] -= SCORE_DECAY;
80+
} else {
81+
// Target path: increment instead of decaying
82+
$dest['score'] += SCORE_INCREMENT;
83+
}
84+
}
85+
unset($dest);
86+
87+
// If path not found, add it
88+
if (!$found) {
89+
$destinations[] = [
90+
'path' => $targetPath,
91+
'score' => SCORE_INCREMENT
92+
];
93+
}
94+
95+
// Remove entries with score <= 0
96+
$destinations = array_filter($destinations, function($dest) {
97+
return $dest['score'] > 0;
98+
});
99+
100+
// Sort by score descending
101+
usort($destinations, function($a, $b) {
102+
return $b['score'] - $a['score'];
103+
});
104+
105+
// Keep only MAX_ENTRIES
106+
if (count($destinations) > MAX_ENTRIES) {
107+
$destinations = array_slice($destinations, 0, MAX_ENTRIES);
108+
}
109+
110+
// Re-index array
111+
$destinations = array_values($destinations);
112+
113+
// Save
114+
$data['destinations'] = $destinations;
115+
savePopularDestinations($data);
116+
}
117+
118+
/**
119+
* Get top N popular destinations
120+
* @param int $limit Maximum number of destinations to return (default 5)
121+
* @return array Array of destination paths
122+
*/
123+
function getPopularDestinations($limit = 5) {
124+
$data = loadPopularDestinations();
125+
$destinations = $data['destinations'];
126+
127+
// Sort by score descending (should already be sorted, but just in case)
128+
usort($destinations, function($a, $b) {
129+
return $b['score'] - $a['score'];
130+
});
131+
132+
// Return top N paths
133+
$result = array_slice($destinations, 0, $limit);
134+
135+
return array_map(function($dest) {
136+
return $dest['path'];
137+
}, $result);
138+
}
139+
?>

0 commit comments

Comments
 (0)