Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions src/dialogs/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import restoreTheme from "lib/restoreTheme";
* @property {string} [default]
* @property {function():void} [onCancel]
* @property {function():void} [onHide]
* @property {string} [className]
*/

/**
* @typedef {object} SelectItem
* @property {string} [value]
* @property {string} [text]
* @property {string} [subText]
* @property {string} [icon]
* @property {string} [className]
* @property {string} [title]
* @property {boolean} [disabled]
* @property {string} [letters]
* @property {boolean} [checkbox]
Expand Down Expand Up @@ -52,7 +56,7 @@ function select(title, items, options = {}) {
<strong className="title">{title}</strong>
) : null;
const $select = (
<div className="prompt select">
<div className={`prompt select ${options.className || ""}`}>
{$titleSpan ? [$titleSpan, $list] : $list}
</div>
);
Expand All @@ -72,6 +76,9 @@ function select(title, items, options = {}) {
checkbox: null,
tailElement: null,
ontailclick: null,
subText: null,
className: null,
title: null,
};

// init item options
Expand Down Expand Up @@ -113,18 +120,35 @@ function select(title, items, options = {}) {
});
}

const $text = (
<span
className="text"
innerHTML={DOMPurify.sanitize(itemOptions.text)}
></span>
);
if (itemOptions.subText) {
$text.classList.add("has-sub-text");
$text.append(
<span className="select-sub-text">
<span className="select-sub-text-content">
{itemOptions.subText}
</span>
</span>,
);
}

const $item = tile({
lead,
tail,
text: (
<span
className="text"
innerHTML={DOMPurify.sanitize(itemOptions.text)}
></span>
),
text: $text,
});

$item.tabIndex = "0";
if (itemOptions.className) $item.classList.add(itemOptions.className);
if (itemOptions.title) {
$item.title = itemOptions.title;
$item.setAttribute("aria-label", itemOptions.title);
}
if (itemOptions.disabled) $item.classList.add("disabled");
if (options.default === itemOptions.value) {
$item.classList.add("selected");
Expand Down
73 changes: 73 additions & 0 deletions src/dialogs/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,79 @@
}
}

&.select.recent-select {
width: calc(100vw - 32px);
min-width: 280px;
max-width: 420px;

.title {
justify-content: flex-start;
min-height: 48px;
margin-inline: 18px;
}

ul {
padding: 4px 10px 10px;
}

.tile.recent-entry {
height: 60px;
justify-content: flex-start;

> .icon {
width: 48px;
min-width: 48px;
height: 60px;
font-size: 1.45em;
}

> .text.has-sub-text {
min-width: 0;
font-size: 1rem;
font-weight: 600;
line-height: 1.35;

> .select-sub-text {
display: block;
overflow: hidden;
color: var(--secondary-text-color);
direction: rtl;
font-size: 0.72rem;
font-weight: 400;
opacity: 0.75;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;

> .select-sub-text-content {
direction: ltr;
unicode-bidi: isolate;
}
}
}

> .clearclose {
width: 48px;
min-width: 48px;
height: 48px;
font-size: 1.2em;
opacity: 0.72;
}
}

.tile.recent-clear {
height: 48px;
justify-content: flex-start;

> .icon {
width: 48px;
min-width: 48px;
height: 48px;
font-size: 1.2em;
}
}
}

ul {
overflow-y: auto;
padding: 10px;
Expand Down
50 changes: 34 additions & 16 deletions src/lib/recents.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import select from "dialogs/select";
import escapeStringRegexp from "escape-string-regexp";
import helpers from "utils/helpers";
import Uri from "utils/Uri";
import Url from "utils/Url";

const recents = {
Expand Down Expand Up @@ -78,20 +79,28 @@ const recents = {
*/
select(extra, type = "all", title = strings["open recent"]) {
const all = [];
const MAX = 20;
const shortName = (name) => {
name = helpers.getVirtualPath(name);

if (name.length > MAX) {
return "..." + name.substr(-MAX - 3);
}
return name;
const pathDetails = (url) => {
url = Url.parse(url).url;
const isSafUri = /^content:/.test(url);
const displayPath = isSafUri
? Uri.getDisplayPath(url)
: helpers.getVirtualPath(url);
const documentPath = isSafUri ? Uri.getDisplayPath(url, []) : displayPath;
const name = Url.basename(displayPath) || Url.basename(documentPath);
const location =
Url.dirname(isSafUri ? documentPath : displayPath)?.replace(
/\/$/,
"",
) || "/";

return { name, location, path: documentPath };
};

if (type === "dir" || type === "all") {
let dirs = this.folders;
for (let dir of dirs) {
const { url } = dir;
const { name, location, path } = pathDetails(url);

const dirValue = {
type: "dir",
Expand All @@ -107,8 +116,11 @@ const recents = {

all.push({
value: dirValue,
text: shortName(url),
text: name,
subText: location,
title: path,
icon: "folder",
className: "recent-entry",
tailElement: tailElement,
ontailclick: (e) => {
const $item = e.currentTarget.closest(".tile");
Expand All @@ -123,7 +135,7 @@ const recents = {
let files = this.files;
for (let file of files) {
if (!file) continue;
const name = shortName(Url.parse(file).url);
const { name, location, path } = pathDetails(Url.parse(file).url);

const fileValue = {
type: "file",
Expand All @@ -139,7 +151,10 @@ const recents = {
all.push({
value: fileValue,
text: name,
subText: location,
title: path,
icon: helpers.getIconForFile(name),
className: "recent-entry",
tailElement: tailElement,
ontailclick: (e) => {
const $item = e.currentTarget.closest(".tile");
Expand All @@ -150,19 +165,22 @@ const recents = {
}
}

if (type === "all") all.push(["clear", strings.clear, "icon clearclose"]);

if (extra) {
extra = extra.map((item) => {
item[1] = shortName(item[1]);
return item;
if (type === "all") {
all.push({
value: "clear",
text: strings.clear,
icon: "clearclose",
className: "recent-clear",
});
}

if (extra) {
all.push(...extra);
}

return select(title, all, {
textTransform: false,
className: "recent-select",
});
},
};
Expand Down
81 changes: 81 additions & 0 deletions src/utils/Uri.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,87 @@ export default {
if (docId) return [rootUri, docId].join("::");
else return rootUri;
},
/**
* Converts a SAF content URI into a readable path. When the URI belongs to
* an added storage, its configured name is used as the path root.
*
* @param {string} url
* @param {Array<{name?: string, uri?: string, url?: string}>} [storages]
* @returns {string}
*/
getDisplayPath(url, storages = parseStorageList()) {
try {
const { docId } = this.parse(url);
const document = splitDocId(docId);
let matchedStorage = null;

for (const storage of storages) {
const storageUrl = storage.uri ?? storage.url;
if (!storageUrl) continue;
const isStorageRoot = url === storageUrl;
const isStorageDescendant = url.startsWith(`${storageUrl}::`);
if (!isStorageRoot && !isStorageDescendant) continue;
if (!matchedStorage || storageUrl.length > matchedStorage.url.length) {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
matchedStorage = { storage, url: storageUrl };
}
}

if (matchedStorage) {
const root = splitDocId(this.parse(matchedStorage.url).docId);
let relativePath = document.path;

if (
document.volume === root.volume &&
document.absolute === root.absolute
) {
if (document.path === root.path) {
relativePath = "";
} else if (root.path && document.path.startsWith(`${root.path}/`)) {
relativePath = document.path.slice(root.path.length + 1);
}
}

return [matchedStorage.storage.name || document.volume, relativePath]
.filter(Boolean)
.join("/");
}

return formatDocumentPath(document) || url;
} catch (_) {
return url;
}

function splitDocId(docId) {
if (docId.startsWith("/")) {
return {
absolute: true,
volume: "",
path: docId.replace(/^\/+/, ""),
};
}

const colonIndex = docId.indexOf(":");
if (colonIndex >= 0) {
return {
absolute: false,
volume: docId.slice(0, colonIndex),
path: docId.slice(colonIndex + 1).replace(/^\/+/, ""),
};
}

const [volume = "", ...pathParts] = docId.split("/");
return {
absolute: false,
volume,
path: pathParts.join("/"),
};
}

function formatDocumentPath(document) {
if (document.absolute) return `/${document.path}`;
return [document.volume, document.path].filter(Boolean).join("/");
}
},
/**
* Gets virtual address by replacing root with name i.e. added in file explorer
* @param {string} url
Expand Down
Loading