Compare commits

...

9 Commits

23 changed files with 305 additions and 35 deletions

View File

@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `150.0`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 150.0`!
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `150.0.1`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 150.0.1`!
### Contributing

View File

@@ -1 +1 @@
fb55808f9cdd2172649e551705008af4f98038fe
e097b3c5ca4139a5b88052379a776782e078356b

View File

@@ -79,6 +79,7 @@ zen-icons-picker-svg =
.label = Icons
urlbar-search-mode-zen_actions = Actions
urlbar-search-mode-workspaces = { zen-panel-ui-workspaces-text }
zen-site-data-settings = Settings
zen-generic-manage = Manage

View File

@@ -39,3 +39,6 @@
- name: zen.urlbar.suggestions.quick-actions
value: true
- name: browser.urlbar.shortcuts.workspaces
value: true

View File

@@ -1,12 +1,21 @@
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
index 2d21248256c6c2bfb8dac958133c10e3251ef564..6645211ef09518b41cb737e3186fbd0162ecf700 100644
index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489fb04e8b7a 100644
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
@@ -799,6 +799,7 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) {
@@ -462,6 +462,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
["shortcuts.tabs", true],
["shortcuts.history", true],
["shortcuts.actions", true],
+ ["shortcuts.workspaces", true],
// Boolean to determine if the providers defined in `exposureResults`
// should be displayed in search results. This can be set by a
@@ -799,6 +800,8 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) {
*/
let rootGroup = {
children: [
+ { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_ACTION }] },
+ { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_WORKSPACE }] },
// heuristic
{
maxResultCount: 1,

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
index 08455d8d5da233639ccebc0e77c0810fb4f674c3..78d0e875978b568b79646489c28b125a44ea79fa 100644
index 08455d8d5da233639ccebc0e77c0810fb4f674c3..da8092b561c3dd8864e57f5a52a1a643db29ace1 100644
--- a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
+++ b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
@@ -913,6 +913,7 @@ export class Query {
@@ -10,3 +10,26 @@ index 08455d8d5da233639ccebc0e77c0810fb4f674c3..78d0e875978b568b79646489c28b125a
(!this.context.trimmedSearchString ||
(!this.context.searchMode.engineName && !result.autofill))
) {
@@ -1043,6 +1044,7 @@ function updateSourcesIfEmpty(context) {
lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE,
lazy.UrlbarTokenizer.TYPE.RESTRICT_URL,
lazy.UrlbarTokenizer.TYPE.RESTRICT_ACTION,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_WORKSPACE,
].includes(t.type)
);
@@ -1100,6 +1102,14 @@ function updateSourcesIfEmpty(context) {
acceptedSources.push(source);
}
break;
+ case lazy.UrlbarUtils.RESULT_SOURCE.WORKSPACES:
+ if (
+ restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_WORKSPACE ||
+ !restrictTokenType
+ ) {
+ acceptedSources.push(source);
+ }
+ break;
case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL:
case lazy.UrlbarUtils.RESULT_SOURCE.ADDON:
default:

View File

@@ -0,0 +1,28 @@
diff --git a/browser/components/urlbar/UrlbarTokenizer.sys.mjs b/browser/components/urlbar/UrlbarTokenizer.sys.mjs
index d4af0ee5138a69139b94d898fb07e2345172f025..f750aae3f9f0a849ca009784510575b2b7119e6d 100644
--- a/browser/components/urlbar/UrlbarTokenizer.sys.mjs
+++ b/browser/components/urlbar/UrlbarTokenizer.sys.mjs
@@ -66,6 +66,7 @@ export var UrlbarTokenizer = {
// `looksLikeOrigin()` returned `LOOKS_LIKE_ORIGIN.OTHER` for this token. It
// may or may not be an origin.
POSSIBLE_ORIGIN_BUT_SEARCH_ALLOWED: 12,
+ RESTRICT_WORKSPACE: 13,
}),
// The special characters below can be typed into the urlbar to restrict
@@ -83,6 +84,7 @@ export var UrlbarTokenizer = {
TITLE: "#",
URL: "$",
ACTION: ">",
+ WORKSPACE: "`",
}),
// The keys of characters in RESTRICT that will enter search mode.
@@ -97,6 +99,7 @@ export var UrlbarTokenizer = {
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
keys.push(this.RESTRICT.ACTION);
}
+ keys.push(this.RESTRICT.WORKSPACE);
return new Set(keys);
},

View File

@@ -1,29 +1,50 @@
diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs
index 64afd613f454edd7786fcc1e2f307a582e4d5f51..b4af9cc2fbddba2c5229e8ffee7b9c0c06c3e1d0 100644
index 64afd613f454edd7786fcc1e2f307a582e4d5f51..50e2dd129d4f2e8f0b07e29639d660cd08ee7318 100644
--- a/browser/components/urlbar/UrlbarUtils.sys.mjs
+++ b/browser/components/urlbar/UrlbarUtils.sys.mjs
@@ -85,6 +85,7 @@ export var UrlbarUtils = {
@@ -85,6 +85,8 @@ export var UrlbarUtils = {
RESTRICT_SEARCH_KEYWORD: "restrictSearchKeyword",
SUGGESTED_INDEX: "suggestedIndex",
TAIL_SUGGESTION: "tailSuggestion",
+ ZEN_ACTION: "zenAction",
+ ZEN_WORKSPACE: "zenWorkspace",
}),
// Defines provider types.
@@ -146,6 +147,7 @@ export var UrlbarUtils = {
@@ -146,6 +148,8 @@ export var UrlbarUtils = {
OTHER_NETWORK: 6,
ADDON: 7,
ACTIONS: 8,
+ ZEN_ACTIONS: 9,
+ WORKSPACES: 10,
}),
// Per-result exposure telemetry.
@@ -587,6 +589,8 @@ export var UrlbarUtils = {
@@ -295,6 +299,14 @@ export var UrlbarUtils = {
telemetryLabel: "actions",
uiLabel: "urlbar-searchmode-actions",
},
+ {
+ source: this.RESULT_SOURCE.WORKSPACES,
+ restrict: lazy.UrlbarTokenizer.RESTRICT.WORKSPACE,
+ icon: "chrome://browser/skin/zen-icons/selectable/layers.svg",
+ pref: "shortcuts.workspaces",
+ telemetryLabel: "workspaces",
+ uiLabel: "urlbar-searchmode-workspaces",
+ },
]);
},
@@ -587,6 +599,12 @@ export var UrlbarUtils = {
return this.RESULT_GROUP.HEURISTIC_FALLBACK;
case "UrlbarProviderHistoryUrlHeuristic":
return this.RESULT_GROUP.HEURISTIC_HISTORY_URL;
+ case "ZenUrlbarProviderGlobalActions":
+ return this.RESULT_GROUP.ZEN_ACTION;
+ if (result.source == this.RESULT_SOURCE.WORKSPACES) {
+ return this.RESULT_GROUP.ZEN_WORKSPACE;
+ } else {
+ return this.RESULT_GROUP.ZEN_ACTION;
+ }
case "UrlbarProviderOmnibox":
return this.RESULT_GROUP.HEURISTIC_OMNIBOX;
case "UrlbarProviderRestrictKeywordsAutofill":

View File

@@ -796,7 +796,8 @@
--fp-enabled: 1;
}
#alltabs-button {
#alltabs-button,
#urlbar-engine-one-off-item-workspaces {
list-style-image: url("chrome://browser/skin/tabs.svg") !important;
}

View File

@@ -1,10 +1,20 @@
diff --git a/toolkit/content/widgets/panel.js b/toolkit/content/widgets/panel.js
index dc3e34847f1b6dfd58f5e036fd7d714ef51c1380..8d8e370ca8549d8208669d4fb344fc8abb54fadd 100644
index dc3e34847f1b6dfd58f5e036fd7d714ef51c1380..e88ab308c5eba01ccf82a0d1b9477555fcb9a5de 100644
--- a/toolkit/content/widgets/panel.js
+++ b/toolkit/content/widgets/panel.js
@@ -137,6 +137,9 @@
@@ -136,7 +136,19 @@
let anchorRoot =
this.anchorNode.closest("toolbarbutton, .anchor-root") ||
this.anchorNode;
+ let toolbox = anchorRoot.closest("#navigator-toolbox");
+ if (toolbox) {
+ // Disable transitions for now, see gh-11667
+ toolbox.style.transition = "none";
+ toolbox.setAttribute("zen-compact-mode-active", "true");
+ anchorRoot.ownerGlobal.setTimeout(() => {
+ toolbox.style.transition = "";
+ }, 0);
+ }
anchorRoot.setAttribute("open", "true");
+ if (anchorRoot.closest("#urlbar") && window.gURLBar) {
+ gURLBar.setAttribute("has-popup-open", "true");
@@ -12,7 +22,7 @@ index dc3e34847f1b6dfd58f5e036fd7d714ef51c1380..8d8e370ca8549d8208669d4fb344fc8a
}
if (this.getAttribute("animate") != "false") {
@@ -183,6 +186,9 @@
@@ -183,6 +195,9 @@
this.anchorNode.closest("toolbarbutton, .anchor-root") ||
this.anchorNode;
anchorRoot.removeAttribute("open");

View File

@@ -88,7 +88,7 @@ export class nsZenBoostStyles {
style += `}\n`;
}
if (boostData.customCSS.trim() != "") {
if ((boostData.customCSS || "").trim() != "") {
style += `/* USER CSS */\n`;
style += `${boostData.customCSS || ""}\n`;
}

View File

@@ -115,7 +115,7 @@
<panel type="arrow" popupalign="topmiddle" id="zen-boost-advanced-color-options-panel">
<vbox>
<p data-l10n-id="zen-bootst-color-contrast"></p>
<html:input id="zen-boost-color-contrast" type="range" min="0.1" max="0.9" value="0.75" step="0.01"/>
<html:input id="zen-boost-color-contrast" type="range" min="0.05" max="0.9" value="0.75" step="0.01"/>
</vbox>
<vbox>
<p data-l10n-id="zen-bootst-color-brightness"></p>

View File

@@ -43,7 +43,7 @@
}
/* stylelint-disable-next-line media-query-no-invalid */
@media (not -moz-pref("zen.view.shift-down-site-on-hover")) and (not ((-moz-pref("zen.view.experimental-no-window-controls") or (not -moz-pref("zen.view.hide-window-controls"))) and -moz-pref("zen.view.use-single-toolbar"))) {
@media (not -moz-pref("zen.view.shift-down-site-on-hover")) {
.browserSidebarContainer:is(.deck-selected, [zen-split="true"]) .browserContainer {
transition: margin var(--zen-hidden-toolbar-transition);
@@ -59,6 +59,12 @@
--margin-top-fix: calc(-1 * var(--zen-toolbar-height-with-bookmarks) + var(--zen-element-separation));
}
@media -moz-pref("zen.view.experimental-no-window-controls") or (not -moz-pref("zen.view.hide-window-controls")) or (not -moz-pref("browser.tabs.inTitlebar")) {
:root:not([zen-has-bookmarks="true"])[zen-single-toolbar="true"] & {
--margin-top-fix: 0px;
}
}
margin-top: var(--margin-top-fix);
}
}

View File

@@ -250,7 +250,7 @@
--panel-separator-color: color-mix(in srgb, currentColor 15%, transparent) !important;
--zen-big-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
--zen-active-tab-scale: 0.99;
--zen-active-tab-scale: 0.985;
/* Define tab hover background color */
--tab-hover-background-color: var(--toolbarbutton-hover-background);

View File

@@ -952,6 +952,7 @@
// Sometimes, dragend doesn't always get called when dragging
// to different windows, see gh-8643.
delete ownerGlobal.gZenCompactModeManager._isTabBeingDragged;
ownerGlobal.gZenCompactModeManager._clearAllHoverStates();
}
this.clearSpaceSwitchTimer();
gZenFolders.highlightGroupOnDragOver(null);
@@ -1107,7 +1108,10 @@
);
for (let i = startIndex; i <= endIndex; i++) {
let item = items[i];
if (!movingTabs.includes(item)) {
if (
!movingTabs.includes(item) &&
!(isTabGroupLabel(item) && i == startIndex)
) {
tabsInBetween.push(item);
}
}

View File

@@ -1353,6 +1353,9 @@ class nsZenWindowSync {
_forZenEmptyTab: tab.hasAttribute("zen-empty-tab"),
});
newTab.id = tab.id;
if (!tab.hasAttribute("pending")) {
newTab.removeAttribute("pending");
}
this.#syncItemWithOriginal(
tab,
newTab,

View File

@@ -664,7 +664,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
) {
let newIndex = dropIndex;
let fromDifferentWindow = false;
movingTabs = Array.from(movingTabs || draggedTab)
let ownedTabs = Array.from(movingTabs || draggedTab)
.reverse()
.map(tab => {
if (!gBrowser.isTab(tab)) {
@@ -700,6 +700,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
return tab;
});
movingTabs = [...ownedTabs];
if (fromDifferentWindow) {
gBrowser.addRangeToMultiSelectedTabs(
gBrowser.tabContainer.dragAndDropElements[dropIndex],
@@ -822,7 +823,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
} catch (ex) {
console.error("Error moving tabs:", ex);
}
return [draggedTab, movingTabs];
return [draggedTab, ownedTabs];
}
onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI) {

View File

@@ -31,7 +31,7 @@ z-index: 1;
%include ../../compact-mode/windows-captions-fix-default.inc.css
}
@media -moz-pref('zen.view.experimental-no-window-controls') {
@media -moz-pref('zen.view.experimental-no-window-controls') or (not -moz-pref("browser.tabs.inTitlebar")) {
:root:not([zen-has-bookmarks]) & {
max-height: 0 !important;
overflow: hidden;

View File

@@ -1089,7 +1089,7 @@
&:active,
&[open] {
scale: 0.99;
scale: 0.985;
}
&[in-urlbar] {

View File

@@ -5,3 +5,4 @@
[DEFAULT]
["browser_ub_actions_search.js"]
["browser_workspace_restrict_search.js"]

View File

@@ -0,0 +1,142 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
});
UrlbarTestUtils.init(this);
add_task(async function test_Workspace_Search_OneOff_Pref() {
const oneOffSearchButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
async function openPopupAndWaitForRebuild(value = "") {
oneOffSearchButtons.invalidateCache();
const rebuildPromise = BrowserTestUtils.waitForEvent(
oneOffSearchButtons,
"rebuild"
);
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
waitForFocus,
value,
});
await rebuildPromise;
}
function getWorkspaceShortcut() {
return [...oneOffSearchButtons.localButtons].find(
button => button.source == UrlbarUtils.RESULT_SOURCE.WORKSPACES
);
}
try {
await SpecialPowers.pushPrefEnv({
set: [["zen.urlbar.hide-one-offs", false]],
});
await openPopupAndWaitForRebuild();
ok(
getWorkspaceShortcut(),
"The workspace shortcut should be visible when the pref is enabled"
);
await UrlbarTestUtils.promisePopupClose(window);
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.shortcuts.workspaces", false]],
});
try {
await openPopupAndWaitForRebuild();
ok(
!getWorkspaceShortcut(),
"The workspace shortcut should be hidden when the pref is disabled"
);
await UrlbarTestUtils.promisePopupClose(window);
} finally {
await SpecialPowers.popPrefEnv();
}
} finally {
await SpecialPowers.popPrefEnv();
if (UrlbarTestUtils.isPopupOpen(window)) {
await UrlbarTestUtils.promisePopupClose(window);
}
}
});
add_task(async function test_Workspace_Restrict_Search() {
const originalWorkspaceId = gZenWorkspaces.activeWorkspace;
const workspaceName = "zen-urlbar-workspace-proof-617db8";
await gZenWorkspaces.createAndSaveWorkspace(workspaceName);
const createdWorkspace = gZenWorkspaces
.getWorkspaces()
.find(workspace => workspace.name == workspaceName);
ok(createdWorkspace, "Created the workspace used by the urlbar test");
registerCleanupFunction(async function () {
if (UrlbarTestUtils.isPopupOpen(window)) {
await UrlbarTestUtils.promisePopupClose(window, () =>
EventUtils.synthesizeKey("KEY_Escape")
);
}
if (gZenWorkspaces.activeWorkspace != originalWorkspaceId) {
await gZenWorkspaces.changeWorkspace(originalWorkspaceId);
}
if (
gZenWorkspaces
.getWorkspaces()
.some(workspace => workspace.uuid == createdWorkspace.uuid)
) {
await gZenWorkspaces.removeWorkspace(createdWorkspace.uuid);
}
});
await gZenWorkspaces.changeWorkspace(originalWorkspaceId);
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
waitForFocus,
value: "` " + workspaceName,
});
// Wait for the second search started when the typed token enters search mode.
await UrlbarTestUtils.promiseSearchComplete(window);
ok(gURLBar.searchMode, "The urlbar should enter search mode");
Assert.equal(
gURLBar.searchMode.source,
UrlbarUtils.RESULT_SOURCE.WORKSPACES,
"The typed token should enter workspace search mode"
);
Assert.equal(
gURLBar.searchMode.entry,
"typed",
"The workspace search mode should be entered by typing the token"
);
Assert.equal(gURLBar.value, workspaceName, "The token should be stripped");
const resultCount = UrlbarTestUtils.getResultCount(window);
ok(resultCount > 0, "Should show at least one workspace result");
const resultDetails = [];
for (let i = 0; i < resultCount; i++) {
resultDetails.push(await UrlbarTestUtils.getDetailsOfResultAt(window, i));
}
ok(
resultDetails.every(
({ result, source }) =>
source == UrlbarUtils.RESULT_SOURCE.WORKSPACES &&
result.providerName == "ZenUrlbarProviderGlobalActions"
),
"Typing the workspace token should limit results to workspace actions"
);
Assert.equal(
resultDetails[0].result.payload.prettyName,
workspaceName,
"The matching workspace should be shown first"
);
});

View File

@@ -155,6 +155,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
*/
async isActive(queryContext) {
return (
queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.WORKSPACES ||
queryContext.searchMode?.source ==
UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS ||
(lazy.enabledPref &&
@@ -241,10 +242,13 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
*
* @param {string} query The user's search query.
* @param {boolean} isPrefixed Whether the query is prefixed.
* @param {boolean} isWorkspaceSearch Whether this is a workspace search query
*/
async #findMatchingActions(query, isPrefixed) {
async #findMatchingActions(query, isPrefixed, isWorkspaceSearch) {
const window = lazy.BrowserWindowTracker.getTopWindow();
const actions = await this.#getAvailableActions(window);
const actions = isWorkspaceSearch
? this.#getWorkspaceActions(window)
: await this.#getAvailableActions(window);
let results = [];
for (let action of actions) {
if (isPrefixed && query.length < 1) {
@@ -341,13 +345,21 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
async startQuery(queryContext, addCallback) {
const query = queryContext.trimmedLowerCaseSearchString;
const isWorkspaceSearch =
queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.WORKSPACES;
const isPrefixed =
isWorkspaceSearch ||
queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS;
if (!query && !isPrefixed) {
return;
}
const actionsResults = await this.#findMatchingActions(query, isPrefixed);
const actionsResults = await this.#findMatchingActions(
query,
isPrefixed,
isWorkspaceSearch
);
if (!actionsResults.length) {
return;
}
@@ -361,9 +373,12 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
zenCommand: action.command,
dynamicType: DYNAMIC_TYPE_NAME,
zenAction: true,
query: isPrefixed
? action.label.trimStart()
: queryContext.searchString,
// eslint-disable-next-line no-nested-ternary
query: isWorkspaceSearch
? action.extraPayload.prettyName
: isPrefixed
? action.label.trimStart()
: queryContext.searchString,
icon: action.icon,
shortcutContent:
ownerGlobal.gZenKeyboardShortcutsManager.getShortcutDisplayFromCommand(
@@ -378,7 +393,9 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
!isPrefixed;
let result = new lazy.UrlbarResult({
type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
source: UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS,
source: isWorkspaceSearch
? UrlbarUtils.RESULT_SOURCE.WORKSPACES
: UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS,
payload,
highlights: payloadHighlights,
heuristic: shouldBePrioritized,
@@ -398,7 +415,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
zenUrlbarResultsLearner
.sortCommandsByPriority(finalResults)
.forEach(result => {
if (isPrefixed && i === 0 && query.length > 1) {
if (isPrefixed && !isWorkspaceSearch && i === 0 && query.length > 1) {
result.heuristic = true;
delete result.suggestedIndex;
}

View File

@@ -5,8 +5,8 @@
"binaryName": "zen",
"version": {
"product": "firefox",
"version": "150.0",
"candidate": "150.0",
"version": "150.0.1",
"candidate": "150.0.1",
"candidateBuild": 1
},
"buildOptions": {
@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.19.10b",
"displayVersion": "1.19.11b",
"github": {
"repo": "zen-browser/desktop"
},