|
|
@@ -192,6 +192,7 @@
|
|
|
|
|
|
/* ---------- Sidebar ---------- */
|
|
|
.fsSidebar{
|
|
|
+ flex: 0 0 auto;
|
|
|
width: 200px;
|
|
|
min-width: 200px;
|
|
|
padding: 10px 8px 10px 10px;
|
|
|
@@ -229,6 +230,45 @@
|
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
|
|
|
|
+ /* ---------- Sidebar splitter ---------- */
|
|
|
+ /*
|
|
|
+ Drag handle on the sidebar / file list boundary. It is pulled back
|
|
|
+ over the sidebar border with negative margins, so widening the grab
|
|
|
+ area does not push the panes apart.
|
|
|
+ */
|
|
|
+ .fsSplitter{
|
|
|
+ flex: 0 0 auto;
|
|
|
+ align-self: stretch;
|
|
|
+ width: 7px;
|
|
|
+ margin-left: -4px;
|
|
|
+ margin-right: -3px;
|
|
|
+ position: relative;
|
|
|
+ z-index: 100;
|
|
|
+ cursor: col-resize;
|
|
|
+ background: transparent;
|
|
|
+ transition: background-color 0.12s ease;
|
|
|
+ }
|
|
|
+ .fsSplitter:hover,
|
|
|
+ .fsSplitter.dragging{
|
|
|
+ background: var(--fs-accent);
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
+ /*
|
|
|
+ While dragging, the pointer keeps crossing the panes either side of
|
|
|
+ the handle. Without this the cursor flickers back to the default and
|
|
|
+ stray text gets selected on the way past.
|
|
|
+ */
|
|
|
+ body.fsResizing{
|
|
|
+ cursor: col-resize;
|
|
|
+ user-select: none;
|
|
|
+ -webkit-user-select: none;
|
|
|
+ }
|
|
|
+ body.fsResizing *{
|
|
|
+ cursor: col-resize !important;
|
|
|
+ user-select: none;
|
|
|
+ -webkit-user-select: none;
|
|
|
+ }
|
|
|
+
|
|
|
/* ---------- Main column ---------- */
|
|
|
.fsMain{
|
|
|
flex-grow: 1;
|
|
|
@@ -711,6 +751,41 @@
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
+ /* ---------- Right click menu ---------- */
|
|
|
+ /*
|
|
|
+ Matches the File Manager's #contextmenu (file_explorer.css): the label
|
|
|
+ carries the row and a muted Semantic UI glyph sits at the trailing edge,
|
|
|
+ with no glyph at all on the rows the File Manager leaves bare.
|
|
|
+
|
|
|
+ Scoped to the context menu, so the toolbar dropdown (#moreMenu) keeps
|
|
|
+ its leading icons - the same split the File Manager draws between its
|
|
|
+ right click menu and its "more" menu.
|
|
|
+ */
|
|
|
+ #fsRoot #contextMenu{ min-width: 250px; }
|
|
|
+ #fsRoot #contextMenu .fsMenuItem{
|
|
|
+ gap: 7px;
|
|
|
+ padding: 5px 7px;
|
|
|
+ color: var(--fs-text);
|
|
|
+ }
|
|
|
+ /* Label takes up the slack, so the trailing glyph is always right aligned */
|
|
|
+ #fsRoot #contextMenu .fsMenuItem > span{ flex-grow: 1; }
|
|
|
+ /*
|
|
|
+ The colour needs !important: ao.css styles every <i> under a themed
|
|
|
+ body with a blanket rule whose :not() chain scores higher than any
|
|
|
+ reasonable selector here, so it would otherwise repaint the glyph to
|
|
|
+ the label colour and flatten the two-tone look.
|
|
|
+ */
|
|
|
+ #fsRoot #contextMenu .fsMenuItem > i.icon{
|
|
|
+ width: 16px;
|
|
|
+ min-width: 16px;
|
|
|
+ height: auto;
|
|
|
+ margin: 0 0 0 auto;
|
|
|
+ font-size: 14px;
|
|
|
+ text-align: center;
|
|
|
+ color: var(--fs-icon) !important;
|
|
|
+ opacity: 0.6;
|
|
|
+ }
|
|
|
+
|
|
|
/* ---------- Sidebar overlay (narrow windows) ---------- */
|
|
|
.fsScrim{
|
|
|
position: absolute;
|
|
|
@@ -747,6 +822,8 @@
|
|
|
display: none;
|
|
|
}
|
|
|
.fsSidebar.open{ display: block; }
|
|
|
+ /* The drawer floats over the file list, so there is no edge to drag */
|
|
|
+ .fsSplitter{ display: none !important; }
|
|
|
.fsFooter{ padding: 10px 12px; gap: 8px; }
|
|
|
.fsBtn{ min-width: 78px; padding: 0 12px; }
|
|
|
.fsListHead{ padding-left: 14px; padding-right: 12px; }
|
|
|
@@ -815,6 +892,8 @@
|
|
|
<div class="fsSideTitle" locale="sidebar/devices">Devices</div>
|
|
|
<div id="storagelist"></div>
|
|
|
</div>
|
|
|
+ <!-- Drag handle between the sidebar and the file list, desktop layout only -->
|
|
|
+ <div class="fsSplitter" id="sidebarSplitter" title="Resize"></div>
|
|
|
|
|
|
<div class="fsMain">
|
|
|
<div class="fsToolbar">
|
|
|
@@ -977,9 +1056,6 @@
|
|
|
info: STROKE + '<circle cx="12" cy="12" r="9"/><path d="M12 16v-5M12 8h.01"/></svg>',
|
|
|
eye: STROKE + '<path d="M1.8 12S5.4 5 12 5s10.2 7 10.2 7-3.6 7-10.2 7S1.8 12 1.8 12z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
|
folderPlus: STROKE + '<path d="M3 7a2 2 0 0 1 2-2h4l2 3h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M12 12v5M9.5 14.5h5"/></svg>',
|
|
|
- open: STROKE + '<path d="M5 12h14M13 6l6 6-6 6"/></svg>',
|
|
|
- externalLink: STROKE + '<path d="M15 3h6v6"/><path d="M10.5 13.5L21 3"/><path d="M18 13.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h5.5"/></svg>',
|
|
|
- pencil: STROKE + '<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>',
|
|
|
folderOpen: STROKE + '<path d="M3 7a2 2 0 0 1 2-2h4l2 3h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',
|
|
|
clock: STROKE + '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.2 2"/></svg>',
|
|
|
download: STROKE + '<path d="M12 3v12M7 11l5 5 5-5"/><path d="M4 20h16"/></svg>',
|
|
|
@@ -1984,22 +2060,46 @@
|
|
|
updateDetailsFor(selected.toArray());
|
|
|
}
|
|
|
|
|
|
+ /*
|
|
|
+ applocale sets the window title to the localized "fwtitle" once the
|
|
|
+ locale file lands, and this runs again on every selection change - so
|
|
|
+ the whole string is rebuilt from the locale, base word included, or the
|
|
|
+ title would drop back to English the moment something is selected.
|
|
|
+ */
|
|
|
function setWindowTitleForSelection(objectCount){
|
|
|
+ //Same string as the in-dialog title bar, and the fwtitle applocale sets
|
|
|
+ var baseTitle = applocale.getString("title", "Open");
|
|
|
if (objectCount == 0){
|
|
|
- ao_module_setWindowTitle("Open");
|
|
|
+ ao_module_setWindowTitle(baseTitle);
|
|
|
return;
|
|
|
}
|
|
|
- var typeName = "object";
|
|
|
+
|
|
|
+ /*
|
|
|
+ Singular and plural are separate keys rather than one string with
|
|
|
+ the count spliced in: languages that do not inflect simply carry the
|
|
|
+ same text in both, while English still reads "1 file selected".
|
|
|
+ */
|
|
|
+ var countKey = (objectCount > 1) ? "title/itemsSelected" : "title/itemSelected";
|
|
|
+ var fallbackPost = (objectCount > 1) ? " items selected" : " item selected";
|
|
|
if (type == "file"){
|
|
|
- typeName = "file";
|
|
|
+ countKey = (objectCount > 1) ? "title/filesSelected" : "title/fileSelected";
|
|
|
+ fallbackPost = (objectCount > 1) ? " files selected" : " file selected";
|
|
|
}else if (type == "folder"){
|
|
|
- typeName = "folder";
|
|
|
- }
|
|
|
- var desc = typeName + " selected";
|
|
|
- if (objectCount > 1){
|
|
|
- desc = typeName + "s selected";
|
|
|
+ countKey = (objectCount > 1) ? "title/foldersSelected" : "title/folderSelected";
|
|
|
+ fallbackPost = (objectCount > 1) ? " folders selected" : " folder selected";
|
|
|
}
|
|
|
- ao_module_setWindowTitle("Open (" + objectCount + " " + desc + ")");
|
|
|
+
|
|
|
+ /*
|
|
|
+ The phrase is split around the digit - "<pre>2<post>" - because the
|
|
|
+ count does not sit in the same place in every language: Chinese puts
|
|
|
+ it in the middle ("已選取 2 個資料夾") where English trails it
|
|
|
+ ("2 files selected"). Each half carries its own spacing, so a
|
|
|
+ language that wants the digit against the following word can have
|
|
|
+ that too ("파일 2개 선택됨").
|
|
|
+ */
|
|
|
+ var pre = applocale.getString(countKey + "/pre", "");
|
|
|
+ var post = applocale.getString(countKey + "/post", fallbackPost);
|
|
|
+ ao_module_setWindowTitle(baseTitle + " (" + pre + objectCount + post + ")");
|
|
|
}
|
|
|
|
|
|
function confirmSelection(){
|
|
|
@@ -2382,6 +2482,174 @@
|
|
|
$("#sidebarScrim").removeClass("show");
|
|
|
}
|
|
|
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+ /* Sidebar resizing */
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+ /*
|
|
|
+ Same behaviour as the file manager's pane splitter
|
|
|
+ (js/explorer/splitter.js): drag the sidebar edge to resize it and the
|
|
|
+ width is remembered between visits. Remembered per browser rather than
|
|
|
+ per account, as it describes this window on this screen.
|
|
|
+ */
|
|
|
+ var FS_SIDEBAR_WIDTH_KEY = "file_selector/sidebarWidth";
|
|
|
+ var FS_SIDEBAR_DEFAULT_WIDTH = 200; //must match the width in the stylesheet
|
|
|
+ var FS_SIDEBAR_MIN_WIDTH = 150; //below this the root names are unreadable
|
|
|
+ var FS_SIDEBAR_MAX_WIDTH = 420;
|
|
|
+ var FS_MAIN_MIN_WIDTH = 280; //the file list never shrinks past this
|
|
|
+ var FS_SIDEBAR_DRAWER_WIDTH = 560; //matches the overlay media query
|
|
|
+ /*
|
|
|
+ The width the user asked for, which is not always the width on screen:
|
|
|
+ a narrow window can clamp it down. Kept unclamped so the sidebar springs
|
|
|
+ back to the chosen size once there is room for it again.
|
|
|
+ */
|
|
|
+ var sidebarPreferredWidth = FS_SIDEBAR_DEFAULT_WIDTH;
|
|
|
+
|
|
|
+ //Below this width the sidebar is a drawer over the file list, not a column
|
|
|
+ function sidebarIsDrawer(){
|
|
|
+ return window.innerWidth < FS_SIDEBAR_DRAWER_WIDTH;
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ How wide the sidebar may get right now. The ceiling is not a fixed
|
|
|
+ number: it also has to leave FS_MAIN_MIN_WIDTH for the file list, so in
|
|
|
+ a small float window it comes down rather than letting a drag squeeze
|
|
|
+ the listing away.
|
|
|
+ */
|
|
|
+ function sidebarWidthLimits(){
|
|
|
+ var available = $(".fsBody").width() - FS_MAIN_MIN_WIDTH;
|
|
|
+ return {
|
|
|
+ min: FS_SIDEBAR_MIN_WIDTH,
|
|
|
+ max: Math.min(FS_SIDEBAR_MAX_WIDTH, available)
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function clampSidebarWidth(width){
|
|
|
+ var limits = sidebarWidthLimits();
|
|
|
+ var applied = Math.round(width);
|
|
|
+ if (isNaN(applied)){
|
|
|
+ applied = FS_SIDEBAR_DEFAULT_WIDTH;
|
|
|
+ }
|
|
|
+ if (limits.max < limits.min){
|
|
|
+ //Window too narrow to honour both bounds: the floor wins, otherwise
|
|
|
+ //the sidebar would collapse to nothing
|
|
|
+ return limits.min;
|
|
|
+ }
|
|
|
+ return Math.max(limits.min, Math.min(limits.max, applied));
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ Writes width and min-width together: the sidebar carries a min-width in
|
|
|
+ the stylesheet to stop flexbox shrinking it, and leaving that behind
|
|
|
+ would silently pin the pane at its old size on the way down.
|
|
|
+ */
|
|
|
+ function writeSidebarWidth(applied){
|
|
|
+ $("#sidebar").css({
|
|
|
+ "width": applied + "px",
|
|
|
+ "min-width": applied + "px"
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ //The drag path: what the user drags to becomes the remembered width
|
|
|
+ function setSidebarWidth(width){
|
|
|
+ sidebarPreferredWidth = clampSidebarWidth(width);
|
|
|
+ writeSidebarWidth(sidebarPreferredWidth);
|
|
|
+ return sidebarPreferredWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ Re-applies the preferred width to the DOM. Also the window resize path: a
|
|
|
+ width that was legal in a bigger window can be over the ceiling in a
|
|
|
+ smaller one - and back under it when the window grows again, which is why
|
|
|
+ this clamps the preference rather than overwriting it. In the drawer
|
|
|
+ layout the inline width is dropped entirely, so the overlay keeps the
|
|
|
+ size the stylesheet gives it.
|
|
|
+ */
|
|
|
+ function applySidebarWidth(){
|
|
|
+ if (sidebarIsDrawer()){
|
|
|
+ $("#sidebar").css({ "width": "", "min-width": "" });
|
|
|
+ $("#sidebarSplitter").hide();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ $("#sidebarSplitter").show();
|
|
|
+ writeSidebarWidth(clampSidebarWidth(sidebarPreferredWidth));
|
|
|
+ }
|
|
|
+
|
|
|
+ function saveSidebarWidth(width){
|
|
|
+ try {
|
|
|
+ localStorage.setItem(FS_SIDEBAR_WIDTH_KEY, String(Math.round(width)));
|
|
|
+ } catch (e){
|
|
|
+ //Private browsing or a full store - the width simply is not remembered
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function initSidebarSplitter(){
|
|
|
+ var stored = null;
|
|
|
+ try {
|
|
|
+ stored = localStorage.getItem(FS_SIDEBAR_WIDTH_KEY);
|
|
|
+ } catch (e){
|
|
|
+ stored = null;
|
|
|
+ }
|
|
|
+ if (stored != null && stored != ""){
|
|
|
+ var restored = parseInt(stored);
|
|
|
+ if (!isNaN(restored) && restored > 0){
|
|
|
+ sidebarPreferredWidth = restored;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //Clamped on the way in, so a width saved on a wider screen still fits here
|
|
|
+ applySidebarWidth();
|
|
|
+
|
|
|
+ var handle = document.getElementById("sidebarSplitter");
|
|
|
+ if (handle == null){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ Pointer events rather than mousedown/mousemove: setPointerCapture
|
|
|
+ keeps the drag alive when the cursor leaves the window, which a
|
|
|
+ document level mousemove listener does not.
|
|
|
+ */
|
|
|
+ handle.addEventListener("pointerdown", function(evt){
|
|
|
+ if (evt.button != 0 || sidebarIsDrawer()){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ //Stops the drag from selecting text across the panes
|
|
|
+ evt.preventDefault();
|
|
|
+
|
|
|
+ var startX = evt.clientX;
|
|
|
+ var startWidth = $("#sidebar").outerWidth();
|
|
|
+ /*
|
|
|
+ Capture is an optimisation, not a requirement - the listeners
|
|
|
+ below are on the handle itself. It throws if the pointer is no
|
|
|
+ longer active by the time this runs, which must not abort the drag.
|
|
|
+ */
|
|
|
+ try { handle.setPointerCapture(evt.pointerId); } catch(e) {}
|
|
|
+ handle.classList.add("dragging");
|
|
|
+ //Suppresses hover effects and text selection everywhere for the drag
|
|
|
+ $("body").addClass("fsResizing");
|
|
|
+
|
|
|
+ function onMove(e){
|
|
|
+ setSidebarWidth(startWidth + (e.clientX - startX));
|
|
|
+ }
|
|
|
+
|
|
|
+ function onEnd(){
|
|
|
+ try { handle.releasePointerCapture(evt.pointerId); } catch(e) {}
|
|
|
+ handle.removeEventListener("pointermove", onMove);
|
|
|
+ handle.removeEventListener("pointerup", onEnd);
|
|
|
+ handle.removeEventListener("pointercancel", onEnd);
|
|
|
+ handle.classList.remove("dragging");
|
|
|
+ $("body").removeClass("fsResizing");
|
|
|
+ //Saved on release rather than on every move, so a drag writes once
|
|
|
+ saveSidebarWidth(sidebarPreferredWidth);
|
|
|
+ //The grid view fits its tiles to the width of the file area
|
|
|
+ updateFileAreaDensity();
|
|
|
+ }
|
|
|
+
|
|
|
+ handle.addEventListener("pointermove", onMove);
|
|
|
+ handle.addEventListener("pointerup", onEnd);
|
|
|
+ handle.addEventListener("pointercancel", onEnd);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
/* New folder */
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
@@ -2654,10 +2922,10 @@
|
|
|
|
|
|
function buildFolderContextMenuItems(){
|
|
|
var items = [];
|
|
|
- items.push(contextMenuItem("newfolder", ICONS.folderPlus,
|
|
|
+ items.push(contextMenuItem("newfolder", "folder outline",
|
|
|
applocale.getString("ctx/newfolder", "New Folder")));
|
|
|
items.push('<div class="fsMenuSep"></div>');
|
|
|
- items.push(contextMenuItem("folderproperties", ICONS.info,
|
|
|
+ items.push(contextMenuItem("folderproperties", "",
|
|
|
applocale.getString("ctx/folderproperties", "Folder Properties")));
|
|
|
return items.join("");
|
|
|
}
|
|
|
@@ -2677,28 +2945,39 @@
|
|
|
menu.css({left: left + "px", top: top + "px"});
|
|
|
}
|
|
|
|
|
|
+ /*
|
|
|
+ Glyph per row is taken from the File Manager's context menu so the two
|
|
|
+ menus read the same: Open, Rename and Properties carry none there, and
|
|
|
+ both "open elsewhere" rows share the external link arrow.
|
|
|
+ */
|
|
|
function buildContextMenuItems(isDir){
|
|
|
var items = [];
|
|
|
- items.push(contextMenuItem("open", ICONS.open, applocale.getString("ctx/open", "Open")));
|
|
|
+ items.push(contextMenuItem("open", "", applocale.getString("ctx/open", "Open")));
|
|
|
if (!isDir){
|
|
|
- items.push(contextMenuItem("openwith", ICONS.viewGrid, applocale.getString("ctx/openwith", "Open With")));
|
|
|
+ items.push(contextMenuItem("openwith", "external", applocale.getString("ctx/openwith", "Open With")));
|
|
|
}else{
|
|
|
- items.push(contextMenuItem("opennewwindow", ICONS.externalLink,
|
|
|
+ items.push(contextMenuItem("opennewwindow", "external",
|
|
|
applocale.getString("ctx/opennewwindow", "Open in New Window")));
|
|
|
}
|
|
|
items.push('<div class="fsMenuSep"></div>');
|
|
|
- items.push(contextMenuItem("rename", ICONS.pencil, applocale.getString("ctx/rename", "Rename")));
|
|
|
- items.push(contextMenuItem("delete", ICONS.trash, applocale.getString("ctx/delete", "Delete")));
|
|
|
- items.push(contextMenuItem("download", ICONS.download, applocale.getString("ctx/download", "Download")));
|
|
|
+ items.push(contextMenuItem("rename", "", applocale.getString("ctx/rename", "Rename")));
|
|
|
+ items.push(contextMenuItem("delete", "trash", applocale.getString("ctx/delete", "Delete")));
|
|
|
+ items.push(contextMenuItem("download", "download", applocale.getString("ctx/download", "Download")));
|
|
|
items.push('<div class="fsMenuSep"></div>');
|
|
|
- items.push(contextMenuItem("properties", ICONS.info, applocale.getString("ctx/properties", "Properties")));
|
|
|
+ items.push(contextMenuItem("properties", "", applocale.getString("ctx/properties", "Properties")));
|
|
|
return items.join("");
|
|
|
}
|
|
|
|
|
|
- function contextMenuItem(action, iconSVG, label){
|
|
|
+ /*
|
|
|
+ iconClass is a Semantic UI icon name without the trailing "icon", or an
|
|
|
+ empty string for the rows the File Manager draws without a glyph
|
|
|
+ (Open, Rename, Properties).
|
|
|
+ */
|
|
|
+ function contextMenuItem(action, iconClass, label){
|
|
|
return '<div class="fsMenuItem" onclick="onContextAction(\'' + action + '\');">' +
|
|
|
- '<span class="fsMenuIcon">' + iconSVG + '</span>' +
|
|
|
- '<span>' + escapeHTML(label) + '</span></div>';
|
|
|
+ '<span>' + escapeHTML(label) + '</span>' +
|
|
|
+ (iconClass == "" ? "" : '<i class="' + iconClass + ' icon"></i>') +
|
|
|
+ '</div>';
|
|
|
}
|
|
|
|
|
|
function closeContextMenu(){
|
|
|
@@ -2943,10 +3222,16 @@
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+ //Runs here rather than with the other init calls at the top of this
|
|
|
+ //script: the FS_SIDEBAR_* values above are var declarations, so they
|
|
|
+ //only hold their values once execution has reached them
|
|
|
+ initSidebarSplitter();
|
|
|
+
|
|
|
$(window).on("resize", function(){
|
|
|
if (window.innerWidth >= 560){
|
|
|
closeSidebar();
|
|
|
}
|
|
|
+ applySidebarWidth();
|
|
|
updateFileAreaDensity();
|
|
|
});
|
|
|
</script>
|