mirror of
https://github.com/zen-browser/desktop.git
synced 2026-05-24 22:00:13 +00:00
no-bug: Added history search
This commit is contained in:
@@ -23,6 +23,52 @@ library-search-no-results = No results
|
||||
|
||||
library-filter-button = Filter
|
||||
|
||||
library-history-filter-all =
|
||||
.label = All time
|
||||
library-history-filter-today =
|
||||
.label = Today
|
||||
library-history-filter-yesterday =
|
||||
.label = Yesterday
|
||||
library-history-filter-last-7-days =
|
||||
.label = Last 7 days
|
||||
library-history-filter-last-30-days =
|
||||
.label = Last 30 days
|
||||
|
||||
library-downloads-filter-all =
|
||||
.label = All
|
||||
library-downloads-filter-completed =
|
||||
.label = Completed
|
||||
library-downloads-filter-in-progress =
|
||||
.label = In progress
|
||||
library-downloads-filter-failed =
|
||||
.label = Failed
|
||||
library-downloads-filter-paused =
|
||||
.label = Paused
|
||||
|
||||
library-item-context-open =
|
||||
.label = Open
|
||||
library-item-context-open-glance =
|
||||
.label = Open in Glance
|
||||
library-item-context-open-new-tab =
|
||||
.label = Open in New Tab
|
||||
library-item-context-open-new-window =
|
||||
.label = Open in New Window
|
||||
library-item-context-copy-url =
|
||||
.label = Copy URL
|
||||
library-item-context-delete-history =
|
||||
.label = Forget About This Page
|
||||
library-item-context-show-in-folder =
|
||||
.label = Show in Folder
|
||||
library-item-context-open-source =
|
||||
.label = Open Source URL
|
||||
library-item-context-remove =
|
||||
.label = Remove from History
|
||||
|
||||
library-history-action-remove =
|
||||
.tooltiptext = Forget About This Page
|
||||
library-history-action-open-tab =
|
||||
.tooltiptext = Open in New Tab
|
||||
|
||||
library-downloads-state-downloading = Downloading…
|
||||
library-downloads-state-canceled = Canceled
|
||||
library-downloads-state-failed = Failed
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="localization" href="browser/zen-menubar.ftl"/>
|
||||
<link rel="localization" href="browser/zen-vertical-tabs.ftl"/>
|
||||
<link rel="localization" href="browser/zen-folders.ftl"/>
|
||||
<link rel="localization" href="browser/zen-library.ftl"/>
|
||||
<link rel="localization" href="browser/zen-boosts.ftl"/>
|
||||
<link rel="localization" href="browser/zen-live-folders.ftl"/>
|
||||
</linkset>
|
||||
|
||||
@@ -10,11 +10,13 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||
DownloadsCommon:
|
||||
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
|
||||
DownloadsViewUI:
|
||||
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
|
||||
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||||
});
|
||||
|
||||
@@ -24,7 +26,32 @@ ChromeUtils.defineLazyGetter(
|
||||
() => new Localization(["browser/zen-library.ftl"], true)
|
||||
);
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "clipboardHelper", () =>
|
||||
Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper)
|
||||
);
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
const HISTORY_FILTERS = [
|
||||
{ id: "all", label: "library-history-filter-all", days: null },
|
||||
{ id: "today", label: "library-history-filter-today", days: 0 },
|
||||
{ id: "yesterday", label: "library-history-filter-yesterday", days: 1 },
|
||||
{ id: "last-7-days", label: "library-history-filter-last-7-days", days: 7 },
|
||||
{
|
||||
id: "last-30-days",
|
||||
label: "library-history-filter-last-30-days",
|
||||
days: 30,
|
||||
},
|
||||
];
|
||||
|
||||
const DOWNLOAD_FILTERS = [
|
||||
{ id: "all", label: "library-downloads-filter-all" },
|
||||
{ id: "completed", label: "library-downloads-filter-completed" },
|
||||
{ id: "in-progress", label: "library-downloads-filter-in-progress" },
|
||||
{ id: "failed", label: "library-downloads-filter-failed" },
|
||||
{ id: "paused", label: "library-downloads-filter-paused" },
|
||||
];
|
||||
|
||||
// Intl singletons (construction is expensive in SpiderMonkey)
|
||||
const TIME_FORMATTER = new Intl.DateTimeFormat(undefined, {
|
||||
@@ -67,6 +94,7 @@ class SearchSection extends LibrarySection {
|
||||
inputValue: { type: String },
|
||||
isEmpty: { type: Boolean },
|
||||
isScrolledToTop: { type: Boolean },
|
||||
activeFilter: { type: String },
|
||||
};
|
||||
|
||||
#canDebug = false;
|
||||
@@ -80,6 +108,9 @@ class SearchSection extends LibrarySection {
|
||||
if (this.inputValue === undefined) {
|
||||
this.inputValue = this.searchTerm;
|
||||
}
|
||||
if (!this.activeFilter) {
|
||||
this.activeFilter = this._filters()[0]?.id ?? "";
|
||||
}
|
||||
this.isScrolledToTop = true;
|
||||
this.isEmpty = false;
|
||||
super.connectedCallback();
|
||||
@@ -156,17 +187,321 @@ class SearchSection extends LibrarySection {
|
||||
return TIME_FORMATTER.format(d);
|
||||
}
|
||||
|
||||
/** Subclasses override to expose filter options. Empty array hides the button. */
|
||||
_filters() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user picks a filter; subclasses re-query or re-filter.
|
||||
*
|
||||
* @param _id
|
||||
*/
|
||||
_onFilterChange(_id) {}
|
||||
|
||||
_onFilterButtonClick(event) {
|
||||
event.stopPropagation();
|
||||
const filters = this._filters();
|
||||
if (filters.length === 0) {
|
||||
return;
|
||||
}
|
||||
const items = filters.map(f => ({
|
||||
l10nId: f.label,
|
||||
type: "radio",
|
||||
checked: this.activeFilter === f.id,
|
||||
onClick: () => this._onSelectFilter(f.id),
|
||||
}));
|
||||
this._openNativeContextMenu(items, {
|
||||
type: "anchor",
|
||||
el: event.currentTarget,
|
||||
});
|
||||
}
|
||||
|
||||
_onSelectFilter(id) {
|
||||
if (id === this.activeFilter) {
|
||||
return;
|
||||
}
|
||||
this.activeFilter = id;
|
||||
this._onFilterChange(id);
|
||||
}
|
||||
|
||||
_activeFilterLabel() {
|
||||
const filters = this._filters();
|
||||
const active = filters.find(f => f.id === this.activeFilter) ?? filters[0];
|
||||
return active ? this._l10n(active.label) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders one item row. `sideTop`/`sideBottom` are the stacked right-side
|
||||
* info slots (e.g. domain over time for downloads, just time for history).
|
||||
*
|
||||
* @param root0
|
||||
* @param root0.key
|
||||
* @param root0.iconSrc
|
||||
* @param root0.label
|
||||
* @param root0.sublabel
|
||||
* @param root0.sideTop
|
||||
* @param root0.sideBottom
|
||||
* @param root0.payload
|
||||
*/
|
||||
_renderItem({ key, iconSrc, label, sublabel, sideTop, sideBottom, payload }) {
|
||||
return html`
|
||||
<div
|
||||
class="library-item"
|
||||
data-key=${key ?? ""}
|
||||
@click=${e => this._onItemClick(e, payload)}
|
||||
@auxclick=${e => this._onItemClick(e, payload)}
|
||||
@contextmenu=${e => this._onItemContextMenu(e, payload)}
|
||||
>
|
||||
<div class="library-item-stack">
|
||||
<div class="library-item-background"></div>
|
||||
<div class="library-item-content">
|
||||
<div class="library-item-icon-stack">
|
||||
<img
|
||||
class="library-item-icon-image"
|
||||
src=${iconSrc || ""}
|
||||
alt=""
|
||||
role="presentation"
|
||||
/>
|
||||
</div>
|
||||
<div class="library-item-label-container">
|
||||
<label class="library-item-label">${label}</label>
|
||||
<label class="library-item-sublabel">${sublabel ?? ""}</label>
|
||||
</div>
|
||||
${
|
||||
sideTop || sideBottom
|
||||
? html`
|
||||
<div class="library-item-side">
|
||||
${
|
||||
sideTop
|
||||
? html`<label class="library-item-side-top"
|
||||
>${sideTop}</label
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
sideBottom
|
||||
? html`<label class="library-item-side-bottom"
|
||||
>${sideBottom}</label
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<div class="library-item-actions">
|
||||
${this._renderItemActions(payload).map(
|
||||
action => html`
|
||||
<button
|
||||
class="library-item-action-button"
|
||||
tabindex="-1"
|
||||
data-l10n-id=${action.l10nId ?? ""}
|
||||
@click=${e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
action.onClick?.(e);
|
||||
}}
|
||||
@auxclick=${e => e.stopPropagation()}
|
||||
>
|
||||
<img src=${action.icon} alt="" />
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses override to provide trailing action buttons. Default is a single
|
||||
* "more options" button that opens the context menu.
|
||||
*
|
||||
* @param payload
|
||||
*/
|
||||
_renderItemActions(payload) {
|
||||
return [
|
||||
{
|
||||
icon: "chrome://browser/skin/zen-icons/menu.svg",
|
||||
onClick: e => this._onItemMenuButtonClick(e, payload),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses override; default is a no-op.
|
||||
*
|
||||
* @param _event
|
||||
* @param _payload
|
||||
*/
|
||||
_onItemClick(_event, _payload) {}
|
||||
|
||||
/**
|
||||
* Subclasses override to build a menu; return null to skip the menu.
|
||||
*
|
||||
* @param _payload
|
||||
*/
|
||||
_buildContextMenu(_payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_onItemContextMenu(event, payload) {
|
||||
const items = this._buildContextMenu(payload);
|
||||
if (!items?.length) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this._openNativeContextMenu(items, {
|
||||
type: "screen",
|
||||
x: event.screenX,
|
||||
y: event.screenY,
|
||||
});
|
||||
}
|
||||
|
||||
_onItemMenuButtonClick(event, payload) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const items = this._buildContextMenu(payload);
|
||||
if (!items?.length) {
|
||||
return;
|
||||
}
|
||||
this._openNativeContextMenu(items, {
|
||||
type: "anchor",
|
||||
el: event.currentTarget,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a native XUL menupopup, append it to the chrome popup-set, open at
|
||||
* the requested location, and remove it on hide. We rebuild on every open
|
||||
* because the action set depends on the row's payload.
|
||||
*
|
||||
* @param items
|
||||
* @param opener
|
||||
*/
|
||||
_openNativeContextMenu(items, opener) {
|
||||
const popupSet = document.getElementById("mainPopupSet");
|
||||
const popup = document.createXULElement("menupopup");
|
||||
|
||||
for (const item of items) {
|
||||
if (item.separator) {
|
||||
popup.appendChild(document.createXULElement("menuseparator"));
|
||||
continue;
|
||||
}
|
||||
const menuitem = document.createXULElement("menuitem");
|
||||
menuitem.setAttribute("data-l10n-id", item.l10nId);
|
||||
if (item.type) {
|
||||
menuitem.setAttribute("type", item.type);
|
||||
}
|
||||
if (item.checked) {
|
||||
menuitem.setAttribute("checked", "true");
|
||||
}
|
||||
if (item.disabled) {
|
||||
menuitem.setAttribute("disabled", "true");
|
||||
}
|
||||
menuitem.addEventListener(
|
||||
"command",
|
||||
() => {
|
||||
try {
|
||||
item.onClick?.();
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
popup.appendChild(menuitem);
|
||||
}
|
||||
|
||||
popup.addEventListener(
|
||||
"popuphidden",
|
||||
() => {
|
||||
popup.remove();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
popupSet.appendChild(popup);
|
||||
|
||||
if (opener.type === "anchor") {
|
||||
popup.openPopup(opener.el, "after_end", 0, 0, true, false);
|
||||
} else {
|
||||
popup.openPopupAtScreen(opener.x, opener.y, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the modifier configured for Glance activation is held.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
_isGlanceActivation(event) {
|
||||
if (!Services.prefs.getBoolPref("zen.glance.enabled", true)) {
|
||||
return false;
|
||||
}
|
||||
const method = Services.prefs.getStringPref(
|
||||
"zen.glance.activation-method",
|
||||
"ctrl"
|
||||
);
|
||||
return (
|
||||
(method === "ctrl" && event.ctrlKey) ||
|
||||
(method === "alt" && event.altKey) ||
|
||||
(method === "shift" && event.shiftKey) ||
|
||||
(method === "meta" && event.metaKey)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open `url` in a Glance overlay anchored to the clicked item.
|
||||
*
|
||||
* @param event
|
||||
* @param url
|
||||
*/
|
||||
_openInGlance(event, url) {
|
||||
const itemEl = event.currentTarget;
|
||||
const rect = window.windowUtils.getBoundsWithoutFlushing(itemEl);
|
||||
const tabPanelRect = window.windowUtils.getBoundsWithoutFlushing(
|
||||
window.gBrowser.tabpanels
|
||||
);
|
||||
window.gZenGlanceManager.openGlance({
|
||||
url,
|
||||
clientX: rect.left - tabPanelRect.left,
|
||||
clientY: rect.top - tabPanelRect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
}
|
||||
|
||||
/** Close the parent ZenLibrary overlay. */
|
||||
_closeLibrary() {
|
||||
const host = this.getRootNode().host?.closest?.("zen-library");
|
||||
// Section nodes live outside the ZenLibrary lit tree (they are returned
|
||||
// from render()), so walk to the document and toggle via the static API.
|
||||
if (host?.isOpen) {
|
||||
host.constructor.toggle();
|
||||
return;
|
||||
}
|
||||
const lib = document.querySelector("zen-library[open]");
|
||||
if (lib) {
|
||||
lib.constructor.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
renderSearchResults() {
|
||||
return html``;
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasFilters = !!this._filters().length;
|
||||
return html`
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="chrome://browser/content/zen-styles/zen-library.css"
|
||||
/>
|
||||
<link rel="localization" href="browser/zen-library.ftl" />
|
||||
<div class="search-bar">
|
||||
<div class="search-urlbar">
|
||||
<div class="search-urlbar-background"></div>
|
||||
@@ -195,20 +530,30 @@ class SearchSection extends LibrarySection {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="search-filter-button">
|
||||
<img src="chrome://browser/skin/zen-icons/search-glass.svg" alt="" />
|
||||
<label data-l10n-id="library-filter-button"></label>
|
||||
</button>
|
||||
${
|
||||
hasFilters
|
||||
? html`
|
||||
<button
|
||||
class="search-filter-button"
|
||||
@click=${this._onFilterButtonClick}
|
||||
>
|
||||
<img
|
||||
src="chrome://browser/skin/zen-icons/permissions.svg"
|
||||
alt=""
|
||||
/>
|
||||
<label>${this._activeFilterLabel()}</label>
|
||||
</button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
class="search-results"
|
||||
@scroll=${() => {
|
||||
const el = this.renderRoot.querySelector(".search-results");
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
this.isScrolledToTop = el.scrollTop === 0;
|
||||
}}
|
||||
@scroll=${e => {
|
||||
const el = e.currentTarget;
|
||||
this.isScrolledToTop = el.scrollTop === 0;
|
||||
this._onScroll?.(el);
|
||||
}}
|
||||
?scrolled-to-top=${this.isScrolledToTop}
|
||||
>
|
||||
${this.renderSearchResults()}
|
||||
@@ -223,10 +568,14 @@ class SearchSection extends LibrarySection {
|
||||
* so the main thread stays responsive and the scrollbar tracks correctly.
|
||||
*/
|
||||
class ProgressiveSearchSection extends SearchSection {
|
||||
static INITIAL_BATCH = 50;
|
||||
static SCROLL_BATCH = 50;
|
||||
static SCROLL_THRESHOLD_PX = 400;
|
||||
|
||||
_allItems = [];
|
||||
|
||||
#renderedItemCount = 0;
|
||||
#renderTask = null;
|
||||
#fillScheduled = false;
|
||||
|
||||
_getRenderedSlice() {
|
||||
return this._allItems.slice(0, this.#renderedItemCount);
|
||||
@@ -234,47 +583,83 @@ class ProgressiveSearchSection extends SearchSection {
|
||||
|
||||
/** Call when _allItems is fully replaced (new query / search term). */
|
||||
_resetProgressiveRender() {
|
||||
this.#cancelTask();
|
||||
this.#renderedItemCount = Math.min(50, this._allItems.length);
|
||||
this.isEmpty = this._allItems.length === 0;
|
||||
this.#scheduleNextBatch();
|
||||
}
|
||||
|
||||
/** Call when _allItems grows incrementally to avoid resetting scroll position. */
|
||||
_maintainProgressiveRender() {
|
||||
this.#renderedItemCount = Math.max(
|
||||
this.#renderedItemCount,
|
||||
Math.min(50, this._allItems.length)
|
||||
this.#renderedItemCount = Math.min(
|
||||
ProgressiveSearchSection.INITIAL_BATCH,
|
||||
this._allItems.length
|
||||
);
|
||||
this.isEmpty = this._allItems.length === 0;
|
||||
this.#scheduleNextBatch();
|
||||
this.#scheduleViewportFill();
|
||||
}
|
||||
|
||||
#scheduleNextBatch() {
|
||||
if (this.#renderedItemCount >= this._allItems.length) {
|
||||
/** Call when _allItems grows/shrinks but the rendered prefix should stick. */
|
||||
_maintainProgressiveRender() {
|
||||
this.#renderedItemCount = Math.min(
|
||||
Math.max(
|
||||
this.#renderedItemCount,
|
||||
Math.min(ProgressiveSearchSection.INITIAL_BATCH, this._allItems.length)
|
||||
),
|
||||
this._allItems.length
|
||||
);
|
||||
this.isEmpty = this._allItems.length === 0;
|
||||
this.#scheduleViewportFill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Top up the rendered slice until the list is at least one viewport tall;
|
||||
* a list shorter than the viewport never fires a scroll event, so without
|
||||
* this the user could never trigger more loads.
|
||||
*/
|
||||
#scheduleViewportFill() {
|
||||
if (this.#fillScheduled) {
|
||||
return;
|
||||
}
|
||||
this.#renderTask = requestIdleCallback(() => {
|
||||
this.#renderTask = null;
|
||||
this.#renderedItemCount = Math.min(
|
||||
this.#renderedItemCount + 100,
|
||||
this._allItems.length
|
||||
);
|
||||
this.requestUpdate();
|
||||
this.#scheduleNextBatch();
|
||||
this.#fillScheduled = true;
|
||||
this.updateComplete.then(() => {
|
||||
this.#fillScheduled = false;
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
const el = this.renderRoot.querySelector(".search-results");
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
el.scrollHeight <= el.clientHeight &&
|
||||
this.#renderedItemCount < this._allItems.length
|
||||
) {
|
||||
this.#growBy(ProgressiveSearchSection.SCROLL_BATCH);
|
||||
this.#scheduleViewportFill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#cancelTask() {
|
||||
if (this.#renderTask) {
|
||||
cancelIdleCallback(this.#renderTask);
|
||||
this.#renderTask = null;
|
||||
#growBy(n) {
|
||||
if (this.#renderedItemCount >= this._allItems.length) {
|
||||
return;
|
||||
}
|
||||
this.#renderedItemCount = Math.min(
|
||||
this.#renderedItemCount + n,
|
||||
this._allItems.length
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called from SearchSection's results scroll handler.
|
||||
*
|
||||
* @param el
|
||||
*/
|
||||
_onScroll(el) {
|
||||
if (this.#renderedItemCount >= this._allItems.length) {
|
||||
return;
|
||||
}
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
if (distanceFromBottom <= ProgressiveSearchSection.SCROLL_THRESHOLD_PX) {
|
||||
this.#growBy(ProgressiveSearchSection.SCROLL_BATCH);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// Cancel before super so no stale idle callback fires against cleared base state.
|
||||
this.#cancelTask();
|
||||
this._allItems = [];
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
@@ -378,13 +763,25 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
|
||||
return;
|
||||
}
|
||||
|
||||
const cutoff = this.#filterCutoff();
|
||||
const yesterdayWindow = this.#yesterdayWindow();
|
||||
|
||||
// RESULTS_AS_VISIT produces one node per visit; de-duplicate per day-bucket.
|
||||
const seenNodes = new Set();
|
||||
for (let i = 0; i < root.childCount; i++) {
|
||||
const node = root.getChild(i);
|
||||
const date = new Date(node.time / 1000);
|
||||
const dayStart = new Date(date).setHours(0, 0, 0, 0);
|
||||
const ts = date.getTime();
|
||||
|
||||
if (yesterdayWindow) {
|
||||
if (ts < yesterdayWindow.start || ts >= yesterdayWindow.end) {
|
||||
continue;
|
||||
}
|
||||
} else if (cutoff !== null && ts < cutoff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dayStart = new Date(date).setHours(0, 0, 0, 0);
|
||||
const key = `${dayStart}|${node.uri}`;
|
||||
if (seenNodes.has(key)) {
|
||||
continue;
|
||||
@@ -399,13 +796,47 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
|
||||
}
|
||||
|
||||
this.log(
|
||||
`#walkAndPopulate — ${this._allItems.length} items, search=${!!this.searchTerm}`
|
||||
`#walkAndPopulate — ${this._allItems.length} items, search=${!!this.searchTerm}, filter=${this.activeFilter}`
|
||||
);
|
||||
} finally {
|
||||
this.#walking = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lower bound (timestamp ms) for the active time-range filter, or null for "all". */
|
||||
#filterCutoff() {
|
||||
const filter = HISTORY_FILTERS.find(f => f.id === this.activeFilter);
|
||||
if (!filter || filter.days === null) {
|
||||
return null;
|
||||
}
|
||||
if (filter.id === "today") {
|
||||
return new Date().setHours(0, 0, 0, 0);
|
||||
}
|
||||
if (filter.id === "yesterday") {
|
||||
return null; // handled by #yesterdayWindow
|
||||
}
|
||||
return new Date().setHours(0, 0, 0, 0) - filter.days * DAY_MS;
|
||||
}
|
||||
|
||||
/** Yesterday is a single-day window, not just a lower bound. */
|
||||
#yesterdayWindow() {
|
||||
if (this.activeFilter !== "yesterday") {
|
||||
return null;
|
||||
}
|
||||
const start = new Date().setHours(0, 0, 0, 0) - DAY_MS;
|
||||
return { start, end: start + DAY_MS };
|
||||
}
|
||||
|
||||
_filters() {
|
||||
return HISTORY_FILTERS;
|
||||
}
|
||||
|
||||
_onFilterChange(_id) {
|
||||
this.#walkAndPopulate();
|
||||
this._resetProgressiveRender();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
#createObserver() {
|
||||
const rebuild = () => this.#scheduleRebuild();
|
||||
return {
|
||||
@@ -494,42 +925,128 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
|
||||
if (dayLabel !== lastLabel) {
|
||||
lastLabel = dayLabel;
|
||||
rows.push(html`
|
||||
<label class="library-date-separator"> ${lastLabel} </label>
|
||||
<label class="library-date-separator">${lastLabel}</label>
|
||||
`);
|
||||
}
|
||||
|
||||
rows.push(html`
|
||||
<div class="library-item">
|
||||
<div class="library-item-stack">
|
||||
<div class="library-item-background"></div>
|
||||
<div class="library-item-content">
|
||||
<div class="library-item-icon-stack">
|
||||
<img
|
||||
class="library-item-icon-image"
|
||||
src="page-icon:${v.url || ""}"
|
||||
alt=""
|
||||
role="presentation"
|
||||
/>
|
||||
</div>
|
||||
<div class="library-item-label-container">
|
||||
<label class="library-item-label">${v.title || v.url}</label>
|
||||
<label class="library-item-sublabel"
|
||||
>${
|
||||
v.url?.replace(/^[\w-]+:\/\//, "").replace(/\/$/, "") || ""
|
||||
}</label
|
||||
>
|
||||
</div>
|
||||
<label class="library-item-time"
|
||||
>${this._formatTime(v.date)}</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
const domain =
|
||||
v.url?.replace(/^[\w-]+:\/\//, "").replace(/\/$/, "") || "";
|
||||
const time = this._formatTime(v.date);
|
||||
const sublabel = [domain, time].filter(Boolean).join(" · ");
|
||||
rows.push(
|
||||
this._renderItem({
|
||||
key: `${v.date.getTime()}|${v.url}`,
|
||||
iconSrc: `page-icon:${v.url || ""}`,
|
||||
label: v.title || v.url,
|
||||
sublabel,
|
||||
payload: v,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return html`${rows}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plain click on a history row opens the page in a Glance overlay; holding
|
||||
* Ctrl (or any other modifier / middle-click) falls back to the standard
|
||||
* "where to open" routing.
|
||||
*
|
||||
* @param event
|
||||
* @param item
|
||||
*/
|
||||
_onItemClick(event, item) {
|
||||
if (event.button === 2) {
|
||||
return;
|
||||
}
|
||||
if (!item?.url) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
const hasModifier =
|
||||
event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
|
||||
const isMiddleClick = event.button === 1;
|
||||
|
||||
if (
|
||||
!hasModifier &&
|
||||
!isMiddleClick &&
|
||||
Services.prefs.getBoolPref("zen.glance.enabled", true)
|
||||
) {
|
||||
this._openInGlance(event, item.url);
|
||||
this._closeLibrary();
|
||||
return;
|
||||
}
|
||||
|
||||
const where = lazy.BrowserUtils.whereToOpenLink(event, false, true);
|
||||
window.openTrustedLinkIn(item.url, where === "current" ? "tab" : where);
|
||||
this._closeLibrary();
|
||||
}
|
||||
|
||||
_renderItemActions(item) {
|
||||
if (!item?.url) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
icon: "chrome://browser/skin/zen-icons/edit-delete.svg",
|
||||
l10nId: "library-history-action-remove",
|
||||
onClick: () => {
|
||||
lazy.PlacesUtils.history.remove(item.url).catch(console.error);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "chrome://browser/skin/zen-icons/arrow-right.svg",
|
||||
l10nId: "library-history-action-open-tab",
|
||||
onClick: () => {
|
||||
window.openTrustedLinkIn(item.url, "tab");
|
||||
this._closeLibrary();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
_buildContextMenu(item) {
|
||||
if (!item?.url) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
{
|
||||
l10nId: "library-item-context-open",
|
||||
onClick: () => {
|
||||
window.openTrustedLinkIn(item.url, "tab");
|
||||
this._closeLibrary();
|
||||
},
|
||||
},
|
||||
{
|
||||
l10nId: "library-item-context-open-glance",
|
||||
onClick: () => {
|
||||
// Use the section element as the anchor — context menu opens at
|
||||
// the cursor and the original item element isn't tracked here.
|
||||
const fakeEvent = { currentTarget: this };
|
||||
this._openInGlance(fakeEvent, item.url);
|
||||
this._closeLibrary();
|
||||
},
|
||||
},
|
||||
{
|
||||
l10nId: "library-item-context-open-new-window",
|
||||
onClick: () => {
|
||||
window.openTrustedLinkIn(item.url, "window");
|
||||
this._closeLibrary();
|
||||
},
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
l10nId: "library-item-context-copy-url",
|
||||
onClick: () => lazy.clipboardHelper.copyString(item.url),
|
||||
},
|
||||
{
|
||||
l10nId: "library-item-context-delete-history",
|
||||
onClick: () => {
|
||||
lazy.PlacesUtils.history.remove(item.url).catch(console.error);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,6 +1150,16 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_filters() {
|
||||
return DOWNLOAD_FILTERS;
|
||||
}
|
||||
|
||||
_onFilterChange(_id) {
|
||||
this.#rebuildItems();
|
||||
this._resetProgressiveRender();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
renderSearchResults() {
|
||||
const slice = this._getRenderedSlice();
|
||||
|
||||
@@ -667,32 +1194,121 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
|
||||
? `moz-icon://${dl.target.path}?size=16`
|
||||
: "moz-icon://.unknown?size=16";
|
||||
|
||||
return html`
|
||||
<div class="library-item">
|
||||
<div class="library-item-stack">
|
||||
<div class="library-item-background"></div>
|
||||
<div class="library-item-content">
|
||||
<div class="library-item-icon-stack">
|
||||
<img
|
||||
class="library-item-icon-image"
|
||||
src=${iconPath}
|
||||
alt=""
|
||||
role="presentation"
|
||||
/>
|
||||
</div>
|
||||
<div class="library-item-label-container">
|
||||
<label class="library-item-label">${nameStr}</label>
|
||||
<label class="library-item-sublabel"
|
||||
>${this.#getStatusLabel(dl)}</label
|
||||
>
|
||||
</div>
|
||||
<label class="library-item-time">
|
||||
${this._formatTime(this.#downloadTime(dl))}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const sublabelParts = [
|
||||
this.#getStatusLabel(dl),
|
||||
this.#sourceDomain(dl),
|
||||
this._formatTime(this.#downloadTime(dl)),
|
||||
].filter(Boolean);
|
||||
|
||||
return this._renderItem({
|
||||
key: `${this.#downloadTime(dl)}|${dl.source.url}`,
|
||||
iconSrc: iconPath,
|
||||
label: nameStr,
|
||||
sublabel: sublabelParts.join(" · "),
|
||||
payload: dl,
|
||||
});
|
||||
}
|
||||
|
||||
#sourceDomain(dl) {
|
||||
const url = dl.source.originalUrl || dl.source.url || "";
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, "");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
_onItemClick(event, dl) {
|
||||
if (event.button === 2) {
|
||||
return;
|
||||
}
|
||||
if (!dl) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
const state = lazy.DownloadsCommon.stateOfDownload(dl);
|
||||
const isFinished = state === lazy.DownloadsCommon.DOWNLOAD_FINISHED;
|
||||
|
||||
// Glance modifier — preview the source URL, regardless of file state.
|
||||
if (event.button !== 1 && this._isGlanceActivation(event)) {
|
||||
const previewUrl =
|
||||
dl.source.referrerInfo?.originalReferrer?.spec || dl.source.url;
|
||||
if (previewUrl) {
|
||||
this._openInGlance(event, previewUrl);
|
||||
this._closeLibrary();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Middle-click or finished file with no path: fall back to opening source URL.
|
||||
if (event.button === 1 || !isFinished || !dl.target.path) {
|
||||
const url = dl.source.originalUrl || dl.source.url;
|
||||
if (url) {
|
||||
const where = lazy.BrowserUtils.whereToOpenLink(event, false, true);
|
||||
window.openTrustedLinkIn(url, where === "current" ? "tab" : where);
|
||||
this._closeLibrary();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: launch the downloaded file with the system handler.
|
||||
if (dl.target.exists !== false && !dl.deleted) {
|
||||
lazy.DownloadsCommon.openDownload(dl).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
_buildContextMenu(dl) {
|
||||
if (!dl) {
|
||||
return null;
|
||||
}
|
||||
const state = lazy.DownloadsCommon.stateOfDownload(dl);
|
||||
const isFinished = state === lazy.DownloadsCommon.DOWNLOAD_FINISHED;
|
||||
const fileExists = isFinished && dl.target.exists !== false && !dl.deleted;
|
||||
const sourceUrl = dl.source.originalUrl || dl.source.url;
|
||||
|
||||
const items = [];
|
||||
if (fileExists) {
|
||||
items.push({
|
||||
l10nId: "library-item-context-open",
|
||||
onClick: () => {
|
||||
lazy.DownloadsCommon.openDownload(dl).catch(console.error);
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
l10nId: "library-item-context-show-in-folder",
|
||||
onClick: () => {
|
||||
try {
|
||||
const file = new lazy.FileUtils.File(dl.target.path);
|
||||
lazy.DownloadsCommon.showDownloadedFile(file);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
},
|
||||
});
|
||||
items.push({ separator: true });
|
||||
}
|
||||
if (sourceUrl) {
|
||||
items.push({
|
||||
l10nId: "library-item-context-open-source",
|
||||
onClick: () => {
|
||||
window.openTrustedLinkIn(sourceUrl, "tab");
|
||||
this._closeLibrary();
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
l10nId: "library-item-context-copy-url",
|
||||
onClick: () => lazy.clipboardHelper.copyString(sourceUrl),
|
||||
});
|
||||
items.push({ separator: true });
|
||||
}
|
||||
items.push({
|
||||
l10nId: "library-item-context-remove",
|
||||
onClick: () => {
|
||||
lazy.DownloadsCommon.deleteDownload(dl).catch(console.error);
|
||||
},
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
#downloadTime(dl) {
|
||||
@@ -723,12 +1339,40 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
|
||||
|
||||
#rebuildItems() {
|
||||
const term = this.searchTerm?.trim().toLowerCase();
|
||||
if (term) {
|
||||
this._allItems = this.#allDownloads.filter(dl =>
|
||||
this.#matchesSearchTerm(dl, term)
|
||||
);
|
||||
} else {
|
||||
this._allItems = [...this.#allDownloads];
|
||||
const filter = this.activeFilter;
|
||||
this._allItems = this.#allDownloads.filter(dl => {
|
||||
if (term && !this.#matchesSearchTerm(dl, term)) {
|
||||
return false;
|
||||
}
|
||||
return this.#matchesStatusFilter(dl, filter);
|
||||
});
|
||||
}
|
||||
|
||||
#matchesStatusFilter(dl, filter) {
|
||||
if (!filter || filter === "all") {
|
||||
return true;
|
||||
}
|
||||
const state = lazy.DownloadsCommon.stateOfDownload(dl);
|
||||
const C = lazy.DownloadsCommon;
|
||||
switch (filter) {
|
||||
case "completed":
|
||||
return state === C.DOWNLOAD_FINISHED;
|
||||
case "in-progress":
|
||||
return (
|
||||
state === C.DOWNLOAD_DOWNLOADING || state === C.DOWNLOAD_NOTSTARTED
|
||||
);
|
||||
case "failed":
|
||||
return (
|
||||
state === C.DOWNLOAD_FAILED ||
|
||||
state === C.DOWNLOAD_CANCELED ||
|
||||
state === C.DOWNLOAD_DIRTY ||
|
||||
state === C.DOWNLOAD_BLOCKED_PARENTAL ||
|
||||
state === C.DOWNLOAD_BLOCKED_CONTENT_ANALYSIS
|
||||
);
|
||||
case "paused":
|
||||
return state === C.DOWNLOAD_PAUSED;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
gap: 4px;
|
||||
margin: 8px 6px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
font-size: 1rem; /* Slightly larger font on macOS */
|
||||
@@ -131,7 +130,6 @@
|
||||
margin-inline: 2px;
|
||||
background: transparent;
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-urlbar-background {
|
||||
@@ -155,7 +153,6 @@
|
||||
gap: 0;
|
||||
padding-inline: var(--urlbar-icon-padding, 9px);
|
||||
padding-block: var(--urlbar-padding-block, 4px);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
& .search-icon {
|
||||
@@ -243,7 +240,12 @@
|
||||
fill-opacity: 0.5;
|
||||
-moz-context-properties: fill, fill-opacity;
|
||||
|
||||
&:hover {
|
||||
& label {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[open] {
|
||||
background: var(--zen-toolbar-element-bg-hover);
|
||||
}
|
||||
|
||||
@@ -264,6 +266,7 @@
|
||||
overflow-y: auto;
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: color-mix(in srgb, currentColor 35%, transparent 65%) transparent;
|
||||
-moz-window-dragging: drag;
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
font-size: 1.25rem; /* Slightly larger font on macOS */
|
||||
@@ -308,6 +311,7 @@
|
||||
border-radius: 0;
|
||||
color: inherit;
|
||||
color-scheme: unset;
|
||||
cursor: pointer;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
@@ -315,6 +319,7 @@
|
||||
display: grid;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
-moz-window-dragging: no-drag;
|
||||
}
|
||||
|
||||
.library-item-background {
|
||||
@@ -338,8 +343,9 @@
|
||||
position: relative;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 12px 10px;
|
||||
padding: 0 10px;
|
||||
overflow: clip;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.library-item-icon-stack {
|
||||
@@ -389,23 +395,76 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* unfinished */
|
||||
.library-item-time {
|
||||
.library-item-side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
margin-inline-start: calc(var(--toolbarbutton-inner-padding) - 0.5em); /* idk why this calc makes sense to me
|
||||
but it is inconsistant so please fix it if you have an idea */
|
||||
padding-inline-end: calc(var(--toolbarbutton-inner-padding) / 3);
|
||||
font-size: x-small;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
margin-inline-start: 8px;
|
||||
padding-inline-end: 4px;
|
||||
text-align: end;
|
||||
white-space: nowrap;
|
||||
opacity: 0.65;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.library-item-content:hover & {
|
||||
display: flex;
|
||||
.library-item-side-top {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.library-item-side-bottom {
|
||||
font-size: x-small;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.library-item-actions {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.library-item:hover .library-item-actions,
|
||||
.library-item:focus-within .library-item-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.library-item-action-button {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 6px;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: calc(var(--border-radius-medium) - 6px);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
fill: currentColor;
|
||||
-moz-context-properties: fill, fill-opacity;
|
||||
|
||||
& img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.75;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* unfinished */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user