Skip to content

Commit 0dd1076

Browse files
authored
Improve recent dialog path readability (Acode-Foundation#2567)
* feat: improve recent dialog path readability * fix
1 parent 92f6bef commit 0dd1076

5 files changed

Lines changed: 279 additions & 23 deletions

File tree

src/dialogs/select.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ import restoreTheme from "lib/restoreTheme";
1111
* @property {string} [default]
1212
* @property {function():void} [onCancel]
1313
* @property {function():void} [onHide]
14+
* @property {string} [className]
1415
*/
1516

1617
/**
1718
* @typedef {object} SelectItem
1819
* @property {string} [value]
1920
* @property {string} [text]
21+
* @property {string} [subText]
2022
* @property {string} [icon]
23+
* @property {string} [className]
24+
* @property {string} [title]
2125
* @property {boolean} [disabled]
2226
* @property {string} [letters]
2327
* @property {boolean} [checkbox]
@@ -52,7 +56,7 @@ function select(title, items, options = {}) {
5256
<strong className="title">{title}</strong>
5357
) : null;
5458
const $select = (
55-
<div className="prompt select">
59+
<div className={`prompt select ${options.className || ""}`}>
5660
{$titleSpan ? [$titleSpan, $list] : $list}
5761
</div>
5862
);
@@ -72,6 +76,9 @@ function select(title, items, options = {}) {
7276
checkbox: null,
7377
tailElement: null,
7478
ontailclick: null,
79+
subText: null,
80+
className: null,
81+
title: null,
7582
};
7683

7784
// init item options
@@ -113,18 +120,35 @@ function select(title, items, options = {}) {
113120
});
114121
}
115122

123+
const $text = (
124+
<span
125+
className="text"
126+
innerHTML={DOMPurify.sanitize(itemOptions.text)}
127+
></span>
128+
);
129+
if (itemOptions.subText) {
130+
$text.classList.add("has-sub-text");
131+
$text.append(
132+
<span className="select-sub-text">
133+
<span className="select-sub-text-content">
134+
{itemOptions.subText}
135+
</span>
136+
</span>,
137+
);
138+
}
139+
116140
const $item = tile({
117141
lead,
118142
tail,
119-
text: (
120-
<span
121-
className="text"
122-
innerHTML={DOMPurify.sanitize(itemOptions.text)}
123-
></span>
124-
),
143+
text: $text,
125144
});
126145

127146
$item.tabIndex = "0";
147+
if (itemOptions.className) $item.classList.add(itemOptions.className);
148+
if (itemOptions.title) {
149+
$item.title = itemOptions.title;
150+
$item.setAttribute("aria-label", itemOptions.title);
151+
}
128152
if (itemOptions.disabled) $item.classList.add("disabled");
129153
if (options.default === itemOptions.value) {
130154
$item.classList.add("selected");

src/dialogs/style.scss

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,79 @@
9696
}
9797
}
9898

99+
&.select.recent-select {
100+
width: calc(100vw - 32px);
101+
min-width: 280px;
102+
max-width: 420px;
103+
104+
.title {
105+
justify-content: flex-start;
106+
min-height: 48px;
107+
margin-inline: 18px;
108+
}
109+
110+
ul {
111+
padding: 4px 10px 10px;
112+
}
113+
114+
.tile.recent-entry {
115+
height: 60px;
116+
justify-content: flex-start;
117+
118+
> .icon {
119+
width: 48px;
120+
min-width: 48px;
121+
height: 60px;
122+
font-size: 1.45em;
123+
}
124+
125+
> .text.has-sub-text {
126+
min-width: 0;
127+
font-size: 1rem;
128+
font-weight: 600;
129+
line-height: 1.35;
130+
131+
> .select-sub-text {
132+
display: block;
133+
overflow: hidden;
134+
color: var(--secondary-text-color);
135+
direction: rtl;
136+
font-size: 0.72rem;
137+
font-weight: 400;
138+
opacity: 0.75;
139+
text-align: left;
140+
text-overflow: ellipsis;
141+
white-space: nowrap;
142+
143+
> .select-sub-text-content {
144+
direction: ltr;
145+
unicode-bidi: isolate;
146+
}
147+
}
148+
}
149+
150+
> .clearclose {
151+
width: 48px;
152+
min-width: 48px;
153+
height: 48px;
154+
font-size: 1.2em;
155+
opacity: 0.72;
156+
}
157+
}
158+
159+
.tile.recent-clear {
160+
height: 48px;
161+
justify-content: flex-start;
162+
163+
> .icon {
164+
width: 48px;
165+
min-width: 48px;
166+
height: 48px;
167+
font-size: 1.2em;
168+
}
169+
}
170+
}
171+
99172
ul {
100173
overflow-y: auto;
101174
padding: 10px;

src/lib/recents.js

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import select from "dialogs/select";
22
import escapeStringRegexp from "escape-string-regexp";
33
import helpers from "utils/helpers";
4+
import Uri from "utils/Uri";
45
import Url from "utils/Url";
56

67
const recents = {
@@ -78,20 +79,28 @@ const recents = {
7879
*/
7980
select(extra, type = "all", title = strings["open recent"]) {
8081
const all = [];
81-
const MAX = 20;
82-
const shortName = (name) => {
83-
name = helpers.getVirtualPath(name);
84-
85-
if (name.length > MAX) {
86-
return "..." + name.substr(-MAX - 3);
87-
}
88-
return name;
82+
const pathDetails = (url) => {
83+
url = Url.parse(url).url;
84+
const isSafUri = /^content:/.test(url);
85+
const displayPath = isSafUri
86+
? Uri.getDisplayPath(url)
87+
: helpers.getVirtualPath(url);
88+
const documentPath = isSafUri ? Uri.getDisplayPath(url, []) : displayPath;
89+
const name = Url.basename(displayPath) || Url.basename(documentPath);
90+
const location =
91+
Url.dirname(isSafUri ? documentPath : displayPath)?.replace(
92+
/\/$/,
93+
"",
94+
) || "/";
95+
96+
return { name, location, path: documentPath };
8997
};
9098

9199
if (type === "dir" || type === "all") {
92100
let dirs = this.folders;
93101
for (let dir of dirs) {
94102
const { url } = dir;
103+
const { name, location, path } = pathDetails(url);
95104

96105
const dirValue = {
97106
type: "dir",
@@ -107,8 +116,11 @@ const recents = {
107116

108117
all.push({
109118
value: dirValue,
110-
text: shortName(url),
119+
text: name,
120+
subText: location,
121+
title: path,
111122
icon: "folder",
123+
className: "recent-entry",
112124
tailElement: tailElement,
113125
ontailclick: (e) => {
114126
const $item = e.currentTarget.closest(".tile");
@@ -123,7 +135,7 @@ const recents = {
123135
let files = this.files;
124136
for (let file of files) {
125137
if (!file) continue;
126-
const name = shortName(Url.parse(file).url);
138+
const { name, location, path } = pathDetails(Url.parse(file).url);
127139

128140
const fileValue = {
129141
type: "file",
@@ -139,7 +151,10 @@ const recents = {
139151
all.push({
140152
value: fileValue,
141153
text: name,
154+
subText: location,
155+
title: path,
142156
icon: helpers.getIconForFile(name),
157+
className: "recent-entry",
143158
tailElement: tailElement,
144159
ontailclick: (e) => {
145160
const $item = e.currentTarget.closest(".tile");
@@ -150,19 +165,22 @@ const recents = {
150165
}
151166
}
152167

153-
if (type === "all") all.push(["clear", strings.clear, "icon clearclose"]);
154-
155-
if (extra) {
156-
extra = extra.map((item) => {
157-
item[1] = shortName(item[1]);
158-
return item;
168+
if (type === "all") {
169+
all.push({
170+
value: "clear",
171+
text: strings.clear,
172+
icon: "clearclose",
173+
className: "recent-clear",
159174
});
175+
}
160176

177+
if (extra) {
161178
all.push(...extra);
162179
}
163180

164181
return select(title, all, {
165182
textTransform: false,
183+
className: "recent-select",
166184
});
167185
},
168186
};

src/utils/Uri.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,87 @@ export default {
8484
if (docId) return [rootUri, docId].join("::");
8585
else return rootUri;
8686
},
87+
/**
88+
* Converts a SAF content URI into a readable path. When the URI belongs to
89+
* an added storage, its configured name is used as the path root.
90+
*
91+
* @param {string} url
92+
* @param {Array<{name?: string, uri?: string, url?: string}>} [storages]
93+
* @returns {string}
94+
*/
95+
getDisplayPath(url, storages = parseStorageList()) {
96+
try {
97+
const { docId } = this.parse(url);
98+
const document = splitDocId(docId);
99+
let matchedStorage = null;
100+
101+
for (const storage of storages) {
102+
const storageUrl = storage.uri ?? storage.url;
103+
if (!storageUrl) continue;
104+
const isStorageRoot = url === storageUrl;
105+
const isStorageDescendant = url.startsWith(`${storageUrl}::`);
106+
if (!isStorageRoot && !isStorageDescendant) continue;
107+
if (!matchedStorage || storageUrl.length > matchedStorage.url.length) {
108+
matchedStorage = { storage, url: storageUrl };
109+
}
110+
}
111+
112+
if (matchedStorage) {
113+
const root = splitDocId(this.parse(matchedStorage.url).docId);
114+
let relativePath = document.path;
115+
116+
if (
117+
document.volume === root.volume &&
118+
document.absolute === root.absolute
119+
) {
120+
if (document.path === root.path) {
121+
relativePath = "";
122+
} else if (root.path && document.path.startsWith(`${root.path}/`)) {
123+
relativePath = document.path.slice(root.path.length + 1);
124+
}
125+
}
126+
127+
return [matchedStorage.storage.name || document.volume, relativePath]
128+
.filter(Boolean)
129+
.join("/");
130+
}
131+
132+
return formatDocumentPath(document) || url;
133+
} catch (_) {
134+
return url;
135+
}
136+
137+
function splitDocId(docId) {
138+
if (docId.startsWith("/")) {
139+
return {
140+
absolute: true,
141+
volume: "",
142+
path: docId.replace(/^\/+/, ""),
143+
};
144+
}
145+
146+
const colonIndex = docId.indexOf(":");
147+
if (colonIndex >= 0) {
148+
return {
149+
absolute: false,
150+
volume: docId.slice(0, colonIndex),
151+
path: docId.slice(colonIndex + 1).replace(/^\/+/, ""),
152+
};
153+
}
154+
155+
const [volume = "", ...pathParts] = docId.split("/");
156+
return {
157+
absolute: false,
158+
volume,
159+
path: pathParts.join("/"),
160+
};
161+
}
162+
163+
function formatDocumentPath(document) {
164+
if (document.absolute) return `/${document.path}`;
165+
return [document.volume, document.path].filter(Boolean).join("/");
166+
}
167+
},
87168
/**
88169
* Gets virtual address by replacing root with name i.e. added in file explorer
89170
* @param {string} url

0 commit comments

Comments
 (0)