diff --git a/README.md b/README.md
index 26e5634e7..e97baf11b 100644
--- a/README.md
+++ b/README.md
@@ -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.2`! 🚀
+- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 150.0.2`!
### Contributing
diff --git a/build/AppDir/zen.desktop b/build/AppDir/zen.desktop
index a889e53b4..7dede46fe 100644
--- a/build/AppDir/zen.desktop
+++ b/build/AppDir/zen.desktop
@@ -1,31 +1,698 @@
[Desktop Entry]
Name=Zen Browser
-Comment=Experience tranquillity while browsing the web without people tracking you!
+Comment=A fast, private and secure web browser built to improve your day-to-day experience.
Exec=zen %u
Icon=zen
Type=Application
-MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;application/x-xpinstall;application/pdf;application/json;
+MimeType=application/json;application/pdf;application/rdf+xml;application/rss+xml;application/x-xpinstall;application/xhtml+xml;application/xml;audio/flac;audio/ogg;audio/webm;image/avif;image/gif;image/jpeg;image/png;image/svg+xml;image/webp;text/html;text/xml;video/ogg;video/webm;x-scheme-handler/chrome;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/mailto;
StartupWMClass=zen
Categories=Network;WebBrowser;
StartupNotify=true
Terminal=false
X-MultipleArgs=false
+GenericName=Web Browser
+GenericName[ach]=Web Browser
+GenericName[af]=Web Browser
+GenericName[an]=Web Browser
+GenericName[ar]=متصفح الوِب
+GenericName[ast]=Web Browser
+GenericName[az]=Web Browser
+GenericName[be]=Вэб-браўзер
+GenericName[bg]=Уеб браузър
+GenericName[bn]=ওয়েব ব্রাউজার
+GenericName[bqi]=گشت گر وب
+GenericName[br]=Merdeer Web
+GenericName[brx]=Web Browser
+GenericName[bs]=Web pretraživač
+GenericName[ca]=Navegador web
+GenericName[ca_valencia]=Web Browser
+GenericName[cak]=Web Okik'amaya'l
+GenericName[ckb]=Web Browser
+GenericName[cs]=Webový prohlížeč
+GenericName[cy]=Porwr Gwe
+GenericName[da]=Webbrowser
+GenericName[de]=Internet-Browser
+GenericName[dsb]=Webwobglědowak
+GenericName[el]=Πρόγραμμα περιήγησης
+GenericName[en_CA]=Web Browser
+GenericName[en_GB]=Web Browser
+GenericName[eo]=Retumilo
+GenericName[es_AR]=Navegador web
+GenericName[es_CL]=Navegador Web
+GenericName[es_ES]=Navegador web
+GenericName[es_MX]=Navegador Web
+GenericName[et]=Web Browser
+GenericName[eu]=Web nabigatzailea
+GenericName[fa]=مرورگر وب
+GenericName[ff]=Web Browser
+GenericName[fi]=Verkkoselain
+GenericName[fr]=Navigateur web
+GenericName[fur]=Navigadôr Web
+GenericName[fy_NL]=Webbrowser
+GenericName[ga_IE]=Web Browser
+GenericName[gd]=Brabhsair-lìn
+GenericName[gl]=Navegador web
+GenericName[gn]=Ñanduti Kundahára
+GenericName[gu_IN]=Web Browser
+GenericName[he]=דפדפן אינטרנט
+GenericName[hi_IN]=Web Browser
+GenericName[hr]=Web preglednik
+GenericName[hsb]=Webwobhladowak
+GenericName[hu]=Webböngésző
+GenericName[hy_AM]=Վեբ դիտարկիչ
+GenericName[hye]=Web Browser
+GenericName[ia]=Navigator web
+GenericName[id]=Peramban Web
+GenericName[is]=Vafri
+GenericName[it]=Browser web
+GenericName[ja]=ウェブブラウザー
+GenericName[ka]=ბრაუზერი
+GenericName[kab]=Iminig web
+GenericName[kk]=Веб-браузері
+GenericName[km]=Web Browser
+GenericName[kn]=Web Browser
+GenericName[ko]=웹 브라우저
+GenericName[lij]=Navegatô Web
+GenericName[lo]=ຕົວທ່ອງເວັບເວັບໄຊຕ໌
+GenericName[lt]=Web Browser
+GenericName[ltg]=Web Browser
+GenericName[lv]=Tīmekļa pārlūks
+GenericName[meh]=Web Browser
+GenericName[mk]=Web Browser
+GenericName[ml]=ഗോളാന്തരവല അന്വേഷിയന്ത്രം
+GenericName[mr]=Web Browser
+GenericName[ms]=Web Browser
+GenericName[my]=Web Browser
+GenericName[nb_NO]=Nettleser
+GenericName[ne_NP]=वेब ब्राउजर
+GenericName[nl]=Webbrowser
+GenericName[nn_NO]=Nettlesar
+GenericName[oc]=Navegador Web
+GenericName[pa_IN]=ਵੈੱਬ ਬਰਾਊਜ਼ਰ
+GenericName[pl]=Przeglądarka internetowa
+GenericName[pt_BR]=Navegador web
+GenericName[pt_PT]=Navegador Web
+GenericName[rm]=Navigatur web
+GenericName[ro]=Browser web
+GenericName[ru]=Веб-браузер
+GenericName[sat]=ᱣᱮᱵᱽ ᱵᱽᱨᱟᱣᱡᱚᱨ
+GenericName[sc]=Navigadore web
+GenericName[sco]=Web Browser
+GenericName[si]=වියමන අතිරික්සුව
+GenericName[sk]=Webový prehliadač
+GenericName[skr]=ویب براؤزر
+GenericName[sl]=Spletni brskalnik
+GenericName[son]=Web Browser
+GenericName[sq]=Shfletues
+GenericName[sr]=Веб прегледач
+GenericName[sv_SE]=Webbläsare
+GenericName[szl]=Web Browser
+GenericName[ta]=Web Browser
+GenericName[te]=జాల విహారిణి
+GenericName[tg]=Браузери веб
+GenericName[th]=เว็บเบราว์เซอร์
+GenericName[tl]=Web Browser
+GenericName[tr]=Web Tarayıcısı
+GenericName[trs]=Web riña gāchē nu’
+GenericName[uk]=Браузер
+GenericName[ur]=Web Browser
+GenericName[uz]=Web Browser
+GenericName[vi]=Trình duyệt web
+GenericName[wo]=Web Browser
+GenericName[xh]=Web Browser
+GenericName[zh_CN]=Web 浏览器
+GenericName[zh_TW]=網頁瀏覽器
Keywords=Internet;WWW;Browser;Web;Explorer;
+Keywords[ach]=Internet;WWW;Browser;Web;Explorer;
+Keywords[af]=Internet;WWW;Browser;Web;Explorer;
+Keywords[an]=Internet;WWW;Browser;Web;Explorer;
+Keywords[ar]=إنترنت;WWW;متصفح;ويب;مستكشف;
+Keywords[ast]=Internet;WWW;Browser;Web;Explorer;
+Keywords[az]=Internet;WWW;Browser;Web;Explorer;
+Keywords[be]=Internet;WWW;Browser;Web;Explorer;
+Keywords[bg]=Internet;WWW;Browser;Web;Explorer;
+Keywords[bn]=ইন্টারনেট;WWW;ব্রাউজার;ওয়েব;এক্সপ্লোরার;
+Keywords[bqi]=Internet;WWW;Browser;Web;Explorer;
+Keywords[br]=Internet;WWW;Merdeer;Web;Ergerzhout;
+Keywords[brx]=Internet;WWW;Browser;Web;Explorer;
+Keywords[bs]=Internet;WWW;Pretraživač;Web;Istraživač;
+Keywords[ca]=Internet;WWW;Browser;Web;Explorador;Navegador;
+Keywords[ca_valencia]=Internet;WWW;Browser;Web;Explorer;
+Keywords[cak]=K'amaya'l;WWW;Okik'amaya'l;Kanob'äl;
+Keywords[ckb]=Internet;WWW;Browser;Web;Explorer;
+Keywords[cs]=internet;WWW;prohlížeč;web;
+Keywords[cy]=Rhyngrwyd;WWW;Porwr;Gwe;Archwiliwr;
+Keywords[da]=Internet;WWW;Browser;Nettet;Explorer;
+Keywords[de]=Internet;WWW;Browser;Web;Explorer;
+Keywords[dsb]=Internet;WWW;wobglědowak;Web;Explorer;
+Keywords[el]=Internet;WWW;Browser;Web;Explorer;Διαδίκτυο;Ιστός;Ίντερνετ;
+Keywords[en_CA]=Internet;WWW;Browser;Web;Explorer;
+Keywords[en_GB]=Internet;WWW;Browser;Web;Explorer;
+Keywords[eo]=Interreto;Retumilo;TTT;Teksaĵo;Reto;Internet;WWW;Browser;Web;Explorer;
+Keywords[es_AR]=Internet;WWW;Navegador;Web;Explorador;
+Keywords[es_CL]=Internet;WWW;Navegador;Web;Explorador;
+Keywords[es_ES]=Internet;WWW;Navegador;Web;Explorador;
+Keywords[es_MX]=Internet;WWW;Navegador;Web;Explorador;
+Keywords[et]=Internet;WWW;Browser;Web;Explorer;
+Keywords[eu]=Internet;WWW;Nabigatzailea;Web;Arakatzailea;
+Keywords[fa]=Internet;WWW;Browser;Web;Explorer;
+Keywords[ff]=Internet;WWW;Browser;Web;Explorer;
+Keywords[fi]=Internet;WWW;Browser;Web;Explorer;netti;webbi;selain;
+Keywords[fr]=Internet;WWW;Navigateur;Web;Explorer;
+Keywords[fur]=Internet;WWW;Browser;Navigadôr;Web;Esploradôr;Explorer;
+Keywords[fy_NL]=Ynternet;WWW;Browser;Web;Ferkenner;
+Keywords[ga_IE]=Internet;WWW;Browser;Web;Explorer;
+Keywords[gd]=Internet;WWW;Browser;Web;Explorer;eadar-lìon;brabhsair;brobhsair;lìon;taisgealaiche;
+Keywords[gl]=Internet;WWW;Navegador;Web;Explorador;
+Keywords[gn]=Internet;WWW;Browser;Web;Explorer;
+Keywords[gu_IN]=Internet;WWW;Browser;Web;Explorer;
+Keywords[he]=אינטרנט;WWW;דפדפן;רשת;סייר;מרשתת;
+Keywords[hi_IN]=Internet;WWW;Browser;Web;Explorer;
+Keywords[hr]=Internet;WWW;Preglednik;Web;Istraživač;
+Keywords[hsb]=Internet;WWW;wobhladowak;Web;Explorer;
+Keywords[hu]=Internet;WWW;Böngésző;Web;Világháló;
+Keywords[hy_AM]=Համացանց,WWW,Զննիչ,Վեբ,Ցանցախույզ:
+Keywords[hye]=Internet;WWW;Browser;Web;Explorer;
+Keywords[ia]=Internet;WWW;Navigator;Web;Explorator;
+Keywords[id]=Internet;WWW;Browser;Web;Explorer;
+Keywords[is]=Internet;WWW; Vafri; Vefur; Explorer;
+Keywords[it]=Internet;WWW;Browser;Web;Explorer;Navigatore;
+Keywords[ja]=Internet;WWW;Browser;Web;Explorer;インターネット;ブラウザー;ウェブ;
+Keywords[ka]=ინტერნეტი;WWW;ბრაუზერი;ქსელი;ქსელთან წვდომა;
+Keywords[kab]=Internet;WWW;Browser;Web;Explorer;
+Keywords[kk]=Internet;WWW;Browser;Web;Explorer;Интернет;Ғаламтор;Браузер;Желі;Шолғыш;
+Keywords[km]=Internet;WWW;Browser;Web;Explorer;
+Keywords[kn]=Internet;WWW;Browser;Web;Explorer;
+Keywords[ko]=인터넷;브라우저;웹;탐색기;Internet;WWW;Browser;Web;Explorer;
+Keywords[lij]=Internet;WWW;Browser;Web;Explorer;Navegatô;
+Keywords[lo]=Internet;WWW;Browser;Web;Explorer;
+Keywords[lt]=Internet;WWW;Browser;Web;Explorer;
+Keywords[ltg]=Internet;WWW;Browser;Web;Explorer;
+Keywords[lv]=Internets;WWW;Pārlūkprogramma;Tīmeklis;
+Keywords[meh]=Internet;WWW;Browser;Web;Explorer;
+Keywords[mk]=Internet;WWW;Browser;Web;Explorer;
+Keywords[ml]=ഗോളാന്തരവല;WWW;അന്വേഷിയന്ത്രം;ഗോളാന്തരവല;ആരായൻ;
+Keywords[mr]=Internet;WWW;Browser;Web;Explorer;
+Keywords[ms]=Internet;WWW;Browser;Web;Explorer;
+Keywords[my]=Internet;WWW;Browser;Web;Explorer;
+Keywords[nb_NO]=Internett;WWW;Nettleser;Web;Utforsker;
+Keywords[ne_NP]=Internet;WWW;Browser;Web;Explorer;
+Keywords[nl]=Internet;WWW;Browser;Web;Verkenner;
+Keywords[nn_NO]=Internett;WWW;Nettlesar;Web;Utforskar;
+Keywords[oc]=Internet;WWW;Navegador;Navigador;Navegator;Navigator;Web;Explorer;
+Keywords[pa_IN]=ਇੰਟਰਨੈੱਟ;WWW;ਬਰਾਊਜ਼ਰ;ਵੈੱਬ;ਐਕਸਪਲਰੋਰ;ਵੈਬ;ਇੰਟਰਨੈਟ;
+Keywords[pl]=Internet;WWW;Przeglądarka;Browser;Wyszukiwarka;Web;Sieć;Explorer;Eksplorer;Strony;Witryny;internetowe;
+Keywords[pt_BR]=Internet;WWW;Browser;Web;Explorer;Navegador;
+Keywords[pt_PT]=Internet;WWW;Navegador;Web;Explorador;
+Keywords[rm]=Internet;WWW;Browser;Web;Explorer;navigatur;
+Keywords[ro]=Internet; WWW; Browser; Web; Explorer;
+Keywords[ru]=Сеть;Интернет;Браузер;Доступ в Интернет;
+Keywords[sat]=Internet;WWW;Browser;Web;Explorer;
+Keywords[sc]=Internet;WWW;Navigadore;Web;Explorer;
+Keywords[sco]=Internet;WWW;Browser;Web;Explorer;
+Keywords[si]=අන්තර්ජාලය;අතිරික්සුව;පිරික්සන්න;ගවේශකය;Internet;WWW;Browser;Web;Explorer;
+Keywords[sk]=Internet;WWW;Prehliadač;Web;Prieskumník;
+Keywords[skr]=Internet;WWW;Browser;Web;Explorer;
+Keywords[sl]=internet;www;brskalnik;splet;
+Keywords[son]=Internet;WWW;Browser;Web;Explorer;
+Keywords[sq]=Internet;WWW;Shfletues;Web;Eksplorues;
+Keywords[sr]=Internet;WWW;Browser;Web;Explorer;интернет;њњњ;веб;мрежа;прегледач;експлорер;internet;pregledač;veb;mreža;pregledač;eksplorer;
+Keywords[sv_SE]=Internet;WWW;Webbläsare;Webb;Utforskare;
+Keywords[szl]=Internet;WWW;Browser;Web;Explorer;
+Keywords[ta]=Internet;WWW;Browser;Web;Explorer;
+Keywords[te]=Internet;WWW;Browser;Web;Explorer;
+Keywords[tg]=Интернет;WWW;Браузер;Сомона;Ҷустуҷӯгар;
+Keywords[th]=อินเทอร์เน็ต;เบราว์เซอร์;เว็บ;Internet;WWW;Browser;Web;Explorer;
+Keywords[tl]=Internet;WWW;Browser;Web;Explorer;
+Keywords[tr]=Internet;WWW;Browser;Web;Explorer;İnternet;Tarayıcı;
+Keywords[trs]=Internet;WWW;Browser;Web;Explorer;
+Keywords[uk]=Інтернет;WWW;Браузер;Веб;Переглядач;
+Keywords[ur]=Internet;WWW;Browser;Web;Explorer;
+Keywords[uz]=Internet;WWW;Browser;Web;Explorer;
+Keywords[vi]=Internet;WWW;Trình duyệt;Web;Duyệt web;
+Keywords[wo]=Internet;WWW;Browser;Web;Explorer;
+Keywords[xh]=Internet;WWW;Browser;Web;Explorer;
+Keywords[zh_CN]=Internet;WWW;Browser;Web;Explorer;
+Keywords[zh_TW]=網際網路;網路;瀏覽器;網頁;上網;Internet;WWW;Browser;Web;Explorer;
Actions=new-window;new-blank-window;new-private-window;profilemanager;
X-AppImage-Version=$VERSION
[Desktop Action new-window]
-Name=Open a New Window
Exec=zen %u
+Name=New Window
+Name[ach]=New Window
+Name[af]=New Window
+Name[an]=New Window
+Name[ar]=نافذة جديدة
+Name[ast]=New Window
+Name[az]=New Window
+Name[be]=Новае акно
+Name[bg]=Нов прозорец
+Name[bn]=নতুন উইন্ডো
+Name[bqi]=نیمدری نۊ
+Name[br]=Prenestr nevez
+Name[brx]=New Window
+Name[bs]=Novi prozor
+Name[ca]=Finestra nova
+Name[ca_valencia]=New Window
+Name[cak]=K'ak'a' Tzuwäch
+Name[ckb]=New Window
+Name[cs]=Nové okno
+Name[cy]=Ffenestr Newydd
+Name[da]=Nyt vindue
+Name[de]=Neues Fenster
+Name[dsb]=Nowe wokno
+Name[el]=Νέο παράθυρο
+Name[en_CA]=New Window
+Name[en_GB]=New Window
+Name[eo]=Nova fenestro
+Name[es_AR]=Nueva ventana
+Name[es_CL]=Nueva ventana
+Name[es_ES]=Nueva ventana
+Name[es_MX]=Nueva ventana
+Name[et]=New Window
+Name[eu]=Leiho berria
+Name[fa]=پنجره جدید
+Name[ff]=New Window
+Name[fi]=Uusi ikkuna
+Name[fr]=Nouvelle fenêtre
+Name[fur]=Gnûf barcon
+Name[fy_NL]=Nij finster
+Name[ga_IE]=New Window
+Name[gd]=Uinneag ùr
+Name[gl]=Nova xanela
+Name[gn]=Ovetã pyahu
+Name[gu_IN]=New Window
+Name[he]=חלון חדש
+Name[hi_IN]=New Window
+Name[hr]=Novi prozor
+Name[hsb]=Nowe wokno
+Name[hu]=Új ablak
+Name[hy_AM]=Նոր պատուհան
+Name[hye]=New Window
+Name[ia]=Nove fenestra
+Name[id]=Jendela Baru
+Name[is]=Nýr gluggi
+Name[it]=Nuova finestra
+Name[ja]=新しいウィンドウ
+Name[ka]=ახალი ფანჯარა
+Name[kab]=Asfaylu amaynut
+Name[kk]=Жаңа терезе
+Name[km]=បង្អួចថ្មី
+Name[kn]=New Window
+Name[ko]=새 창
+Name[lij]=Neuvo Barcon
+Name[lo]=ວິນໂດໃໝ່
+Name[lt]=New Window
+Name[ltg]=New Window
+Name[lv]=Jauns logs
+Name[meh]=New Window
+Name[mk]=New Window
+Name[ml]=പുതിയ ജാലകം
+Name[mr]=New Window
+Name[ms]=New Window
+Name[my]=New Window
+Name[nb_NO]=Nytt vindu
+Name[ne_NP]=नयाँ सञ्झ्याल
+Name[nl]=Nieuw venster
+Name[nn_NO]=Nytt vindauge
+Name[oc]=Fenèstra novèla
+Name[pa_IN]=ਨਵੀਂ ਵਿੰਡੋ
+Name[pl]=Nowe okno
+Name[pt_BR]=Nova janela
+Name[pt_PT]=Nova janela
+Name[rm]=Nova fanestra
+Name[ro]=Fereastră nouă
+Name[ru]=Новое окно
+Name[sat]=ᱱᱟᱶᱟ ᱣᱤᱱᱰᱳ
+Name[sc]=Ventana noa
+Name[sco]=New Window
+Name[si]=නව කවුළුව
+Name[sk]=Nové okno
+Name[skr]=نویں ونڈو
+Name[sl]=Novo okno
+Name[son]=New Window
+Name[sq]=Dritare e Re
+Name[sr]=Нови прозор
+Name[sv_SE]=Nytt fönster
+Name[szl]=New Window
+Name[ta]=New Window
+Name[te]=కొత్త కిటికీ
+Name[tg]=Равзанаи нав
+Name[th]=หน้าต่างใหม่
+Name[tl]=New Window
+Name[tr]=Yeni pencere
+Name[trs]=Bēntanâ nākàa
+Name[uk]=Нове вікно
+Name[ur]=New Window
+Name[uz]=New Window
+Name[vi]=Cửa sổ mới
+Name[wo]=New Window
+Name[xh]=New Window
+Name[zh_CN]=新建窗口
+Name[zh_TW]=開新視窗
[Desktop Action new-blank-window]
-Name=Open a New Blank Window
Exec=zen --blank-window %u
+Name=New Blank Window
+Name[ach]=New Blank Window
+Name[af]=New Blank Window
+Name[an]=New Blank Window
+Name[ar]=نافذة فارغة جديدة
+Name[ast]=New Blank Window
+Name[az]=New Blank Window
+Name[be]=Новае пустое акно
+Name[bg]=Нов празен прозорец
+Name[bn]=নতুন ফাঁকা উইন্ডো
+Name[bqi]=نیمدری نۊ خالی
+Name[br]=Prenestr goulloù nevez
+Name[brx]=New Blank Window
+Name[bs]=Novi prazni prozor
+Name[ca]=Finestra en blanc nova
+Name[ca_valencia]=New Blank Window
+Name[cak]=K'ak'a' Tzuwäch K'axk'ol
+Name[ckb]=New Blank Window
+Name[cs]=Nové prázdné okno
+Name[cy]=Ffenestr Wag Newydd
+Name[da]=Nyt tomt vindue
+Name[de]=Neues leeres Fenster
+Name[dsb]=Nowe prázdne wokno
+Name[el]=Νέο κενό παράθυρο
+Name[en_CA]=New Blank Window
+Name[en_GB]=New Blank Window
+Name[eo]=Nova malplena fenestro
+Name[es_AR]=Nueva ventana en blanco
+Name[es_CL]=Nueva ventana en blanco
+Name[es_ES]=Nueva ventana en blanco
+Name[es_MX]=Nueva ventana en blanco
+Name[et]=New Blank Window
+Name[eu]=Leiho huts berri
+Name[fa]=پنجره خالی جدید
+Name[ff]=New Blank Window
+Name[fi]=Uusi tyhjä ikkuna
+Name[fr]=Nouvelle fenêtre vierge
+Name[fur]=Gnûf barcon vuot
+Name[fy_NL]=Nij leeg finster
+Name[ga_IE]=New Blank Window
+Name[gd]=Uinneag bhàn ùr
+Name[gl]=Nova xanela en branco
+Name[gn]=Ovetã ñemi pyahu
+Name[gu_IN]=New Blank Window
+Name[he]=חלון ריק חדש
+Name[hi_IN]=New Blank Window
+Name[hr]=Novi prazni prozor
+Name[hsb]=Nowe prázdne wokno
+Name[hu]=Új üres ablak
+Name[hy_AM]=Նոր դատարկ պատուհան
+Name[hye]=New Blank Window
+Name[ia]=Nove fenestra vacue
+Name[id]=Jendela Kosong Baru
+Name[is]=Nýr tómur gluggi
+Name[it]=Nuova finestra vuota
+Name[ja]=新しい空白のウィンドウ
+Name[ka]=ახალი ცარიელი ფანჯარა
+Name[kab]=Asfaylu amaynut n tunigin tusligt
+Name[kk]=Жаңа бос терезе
+Name[km]=បង្អួចថ្មីឯកជន
+Name[kn]=New Blank Window
+Name[ko]=새 빈 창
+Name[lij]=Neuvo Barcon Vuot
+Name[lo]=ວິນໂດແອ່ງໃໝ່
+Name[lt]=New Blank Window
+Name[ltg]=New Blank Window
+Name[lv]=Jauns tukšais logs
+Name[meh]=New Blank Window
+Name[mk]=New Blank Window
+Name[ml]=പുതിയ ശൂന്യ ജാലകം
+Name[mr]=New Blank Window
+Name[ms]=New Blank Window
+Name[my]=New Blank Window
+Name[nb_NO]=Nytt tomt vindu
+Name[ne_NP]=नयाँ खाली सञ्झ्याल
+Name[nl]=Nieuw leeg venster
+Name[nn_NO]=Nytt tomt vindauge
+Name[oc]=Fenèstra en blanc novèla
+Name[pa_IN]=ਨਵੀਂ ਖਾਲੀ ਵਿੰਡੋ
+Name[pl]=Nowe puste okno
+Name[pt_BR]=Nova janela em branco
+Name[pt_PT]=Nova janela em branco
+Name[rm]=Nova fanestra vacue
+Name[ro]=Fereastră nouă și goală
+Name[ru]=Новое пустое окно
+Name[sat]=ᱱᱟᱶᱟ ᱣᱤᱱᱰᱳ ᱵᱽᱨᱟᱣᱡᱚᱨ
+Name[sc]=Ventana en blanc noa
+Name[sco]=New Blank Window
+Name[si]=නව හිස් කවුළුව
+Name[sk]=Nové prázdné okno
+Name[skr]=نویں خالی ونڈو
+Name[sl]=Novo prazno okno
+Name[son]=New Blank Window
+Name[sq]=Dritare e Re e Pafajshme
+Name[sr]=Нови празни прозор
+Name[sv_SE]=Nytt tomt fönster
+Name[szl]=New Blank Window
+Name[ta]=New Blank Window
+Name[te]=కొత్త ఖాళీ కిటికీ
+Name[tg]=Равзанаи холӣ нав
+Name[th]=หน้าต่างว่างเปล่าใหม่
+Name[tl]=New Blank Window
+Name[tr]=Yeni boş pencere
+Name[trs]=Bēntanâ huì nākàa
+Name[uk]=Нове порожнє вікно
+Name[ur]=New Blank Window
+Name[uz]=New Blank Window
+Name[vi]=Cửa sổ trống mới
+Name[wo]=New Blank Window
+Name[xh]=New Blank Window
+Name[zh_CN]=新建空白窗口
+Name[zh_TW]=開新空白視窗
[Desktop Action new-private-window]
-Name=Open a New Private Window
Exec=zen --private-window %u
+Name=New Private Window
+Name[ach]=New Private Window
+Name[af]=New Private Window
+Name[an]=New Private Window
+Name[ar]=نافذة خاصة جديدة
+Name[ast]=New Private Window
+Name[az]=New Private Window
+Name[be]=Новае прыватнае акно
+Name[bg]=Нов личен прозорец
+Name[bn]=নতুন ব্যক্তিগত উইন্ডো
+Name[bqi]=نیمدری سیخومی نۊ
+Name[br]=Prenestr prevez nevez
+Name[brx]=New Private Window
+Name[bs]=Novi privatni prozor
+Name[ca]=Finestra privada nova
+Name[ca_valencia]=New Private Window
+Name[cak]=K'ak'a' Ichinan Tzuwäch
+Name[ckb]=New Private Window
+Name[cs]=Nové anonymní okno
+Name[cy]=Ffenestr Breifat Newydd
+Name[da]=Nyt privat vindue
+Name[de]=Neues privates Fenster
+Name[dsb]=Nowe priwatne wokno
+Name[el]=Νέο ιδιωτικό παράθυρο
+Name[en_CA]=New Private Window
+Name[en_GB]=New Private Window
+Name[eo]=Nova privata fenestro
+Name[es_AR]=Nueva ventana privada
+Name[es_CL]=Nueva ventana privada
+Name[es_ES]=Nueva ventana privada
+Name[es_MX]=Nueva ventana privada
+Name[et]=New Private Window
+Name[eu]=Leiho pribatu berria
+Name[fa]=پنجره ناشناس جدید
+Name[ff]=New Private Window
+Name[fi]=Uusi yksityinen ikkuna
+Name[fr]=Nouvelle fenêtre privée
+Name[fur]=Gnûf barcon privât
+Name[fy_NL]=Nij priveefinster
+Name[ga_IE]=New Private Window
+Name[gd]=Uinneag phrìobhaideach ùr
+Name[gl]=Nova xanela privada
+Name[gn]=Ovetã ñemi pyahu
+Name[gu_IN]=New Private Window
+Name[he]=חלון פרטי חדש
+Name[hi_IN]=New Private Window
+Name[hr]=Novi privatni prozor
+Name[hsb]=Nowe priwatne wokno
+Name[hu]=Új privát ablak
+Name[hy_AM]=Նոր գաղտնի պատուհան
+Name[hye]=New Private Window
+Name[ia]=Nove fenestra private
+Name[id]=Jendela Mode Pribadi Baru
+Name[is]=Nýr huliðsgluggi
+Name[it]=Nuova finestra anonima
+Name[ja]=新しいプライベートウィンドウ
+Name[ka]=ახალი პირადი ფანჯარა
+Name[kab]=Asfaylu amaynut n tunigin tusligt
+Name[kk]=Жаңа жекелік терезе
+Name[km]=បង្អួចឯកជនថ្មី
+Name[kn]=New Private Window
+Name[ko]=새 사생활 보호 창
+Name[lij]=Neuvo Barcon Privòu
+Name[lo]=ວິນໂດສ່ວນຕົວໃໝ່
+Name[lt]=New Private Window
+Name[ltg]=New Private Window
+Name[lv]=Jauns privātais logs
+Name[meh]=New Private Window
+Name[mk]=New Private Window
+Name[ml]=പുതിയ സ്വകാര്യ ജാലകം
+Name[mr]=New Private Window
+Name[ms]=New Private Window
+Name[my]=New Private Window
+Name[nb_NO]=Nytt privat vindu
+Name[ne_NP]=नयाँ निजी सञ्झ्याल
+Name[nl]=Nieuw privévenster
+Name[nn_NO]=Nytt privat vindauge
+Name[oc]=Fenèstra privada novèla
+Name[pa_IN]=ਨਵੀਂ ਪ੍ਰਾਈਵੇਟ ਵਿੰਡੋ
+Name[pl]=Nowe okno prywatne
+Name[pt_BR]=Nova janela privativa
+Name[pt_PT]=Nova janela privada
+Name[rm]=Nova fanestra privata
+Name[ro]=Fereastră privată nouă
+Name[ru]=Новое приватное окно
+Name[sat]=ᱱᱟᱶᱟ ᱱᱤᱡᱮᱨᱟᱜ ᱣᱤᱱᱰᱳ
+Name[sc]=Ventana privada noa
+Name[sco]=New Private Window
+Name[si]=නව පෞද්. කවුළුව
+Name[sk]=Nové súkromné okno
+Name[skr]=نویں نجی ونڈو
+Name[sl]=Novo zasebno okno
+Name[son]=New Private Window
+Name[sq]=Dritare e Re Private
+Name[sr]=Нови приватни прозор
+Name[sv_SE]=Nytt privat fönster
+Name[szl]=New Private Window
+Name[ta]=New Private Window
+Name[te]=కొత్త ఆంతరంగిక కిటికీ
+Name[tg]=Равзанаи хусусии нав
+Name[th]=หน้าต่างส่วนตัวใหม่
+Name[tl]=New Private Window
+Name[tr]=Yeni gizli pencere
+Name[trs]=Bēntanâ huì nākàa
+Name[uk]=Приватне вікно
+Name[ur]=New Private Window
+Name[uz]=New Private Window
+Name[vi]=Cửa sổ riêng tư mới
+Name[wo]=New Private Window
+Name[xh]=New Private Window
+Name[zh_CN]=新建隐私窗口
+Name[zh_TW]=開新隱私視窗
[Desktop Action profilemanager]
-Name=Open the Profile Manager
Exec=zen --ProfileManager %u
+Name=Open Profile Manager
+Name[ach]=Open Profile Manager
+Name[af]=Open Profile Manager
+Name[an]=Open Profile Manager
+Name[ar]=افتح مدير الملف الشخصي
+Name[ast]=Open Profile Manager
+Name[az]=Open Profile Manager
+Name[be]=Адкрыць менеджар профіляў
+Name[bg]=Отваряне на мениджъра на профили
+Name[bn]=Open Profile Manager
+Name[bqi]=گۊشیڌن دؽوۉداری پوروفایل
+Name[br]=Digeriñ an ardoer aeladoù
+Name[brx]=Open Profile Manager
+Name[bs]=Otvori Menadžera profila
+Name[ca]=Obre el gestor de perfils
+Name[ca_valencia]=Open Profile Manager
+Name[cak]=Open Profile Manager
+Name[ckb]=Open Profile Manager
+Name[cs]=Otevřít Správce profilů
+Name[cy]=Agorwch y Rheolwr Proffil
+Name[da]=Åbn profilhåndtering
+Name[de]=Profilverwaltung öffnen
+Name[dsb]=Profilowy zastojnik wócyniś
+Name[el]=Άνοιγμα Διαχείρισης προφίλ
+Name[en_CA]=Open Profile Manager
+Name[en_GB]=Open Profile Manager
+Name[eo]=Malfermi administranton de profiloj
+Name[es_AR]=Abrir administrador de perfiles
+Name[es_CL]=Abrir administrador de perfiles
+Name[es_ES]=Abrir administrador de perfiles
+Name[es_MX]=Abrir administrador de perfiles
+Name[et]=Open Profile Manager
+Name[eu]=Ireki profilen kudeatzailea
+Name[fa]=گشودن مدیر نمایه
+Name[ff]=Open Profile Manager
+Name[fi]=Avaa profiilien hallinta
+Name[fr]=Ouvrir le gestionnaire de profils
+Name[fur]=Vierç gjestôr profîi
+Name[fy_NL]=Profylbehearder iepenje
+Name[ga_IE]=Open Profile Manager
+Name[gd]=Fosgail manaidsear nam pròifilean
+Name[gl]=Abrir o xestor de perfís
+Name[gn]=Embojuruja mba’ete ñangarekoha
+Name[gu_IN]=Open Profile Manager
+Name[he]=פתיחת מנהל הפרופילים
+Name[hi_IN]=Open Profile Manager
+Name[hr]=Otvori upravljač profila
+Name[hsb]=Zrjadowak profilow wočinić
+Name[hu]=Profilkezelő megnyitása
+Name[hy_AM]=Բացեք պրոֆիլի կառավարիչը
+Name[hye]=Open Profile Manager
+Name[ia]=Aperir le gestor de profilo
+Name[id]=Buka Pengelola Profil
+Name[is]=Opna umsýslu notandasniða
+Name[it]=Apri gestore profili
+Name[ja]=プロファイルマネージャーを開く
+Name[ka]=პროფილის მმართველის გახსნა
+Name[kab]=Ldi amsefrak n umaɣnu
+Name[kk]=Профильдер бақарушысын ашу
+Name[km]=Open Profile Manager
+Name[kn]=Open Profile Manager
+Name[ko]=프로필 관리자 열기
+Name[lij]=Open Profile Manager
+Name[lo]=ເປີດຕົວຈັດການໂປຣໄຟລ໌
+Name[lt]=Open Profile Manager
+Name[ltg]=Open Profile Manager
+Name[lv]=Atvērt profilu pārvaldnieku
+Name[meh]=Open Profile Manager
+Name[mk]=Open Profile Manager
+Name[ml]=രൂപരേഖ മാനേചർ
+Name[mr]=Open Profile Manager
+Name[ms]=Open Profile Manager
+Name[my]=Open Profile Manager
+Name[nb_NO]=Åpne profilbehandler
+Name[ne_NP]=Open Profile Manager
+Name[nl]=Profielbeheerder openen
+Name[nn_NO]=Opne profilhandsaming
+Name[oc]=Dobrir lo gestionari de perfils
+Name[pa_IN]=ਪਰੋਫ਼ਾਈਲ ਮੈਨੇਜਰ ਖੋਲ੍ਹੋ
+Name[pl]=Menedżer profili
+Name[pt_BR]=Abrir gerenciador de perfis
+Name[pt_PT]=Abrir o Gestor de Perfis
+Name[rm]=Avrir l'administraziun da profils
+Name[ro]=Deschide managerul de profiluri
+Name[ru]=Открыть менеджер профилей
+Name[sat]=ᱢᱮᱫᱦᱟᱸ ᱢᱮᱱᱮᱡᱚᱨ ᱠᱷᱩᱞᱟᱹᱭ ᱢᱮ
+Name[sc]=Aberi su gestore de profilos
+Name[sco]=Open Profile Manager
+Name[si]=පැතිකඩ කළමනාකරු අරින්න
+Name[sk]=Otvoriť Správcu profilov
+Name[skr]=پروفائل منیجر کھولو
+Name[sl]=Odpri upravitelja profilov
+Name[son]=Open Profile Manager
+Name[sq]=Hapni Përgjegjës Profilesh
+Name[sr]=Отворите управљач профила
+Name[sv_SE]=Öppna Profilhanteraren
+Name[szl]=Open Profile Manager
+Name[ta]=Open Profile Manager
+Name[te]=Open Profile Manager
+Name[tg]=Кушодани мудири профилҳо
+Name[th]=เปิดตัวจัดการโปรไฟล์
+Name[tl]=Open Profile Manager
+Name[tr]=Profil yöneticisini aç
+Name[trs]=Sa nīkāj ñu’ūnj nej perfî huā nì’nï̀nj ïn
+Name[uk]=Відкрити менеджер профілів
+Name[ur]=Open Profile Manager
+Name[uz]=Open Profile Manager
+Name[vi]=Mở trình quản lý hồ sơ
+Name[wo]=Open Profile Manager
+Name[xh]=Open Profile Manager
+Name[zh_CN]=打开配置文件管理器
+Name[zh_TW]=開啟設定檔管理員
diff --git a/build/firefox-cache/l10n-last-commit-hash b/build/firefox-cache/l10n-last-commit-hash
index fc5481288..9b5d1a99d 100644
--- a/build/firefox-cache/l10n-last-commit-hash
+++ b/build/firefox-cache/l10n-last-commit-hash
@@ -1 +1 @@
-fb55808f9cdd2172649e551705008af4f98038fe
\ No newline at end of file
+73901ca17f4a2159dd4488cea8684e9abbfdcc89
\ No newline at end of file
diff --git a/locales/en-US/browser/browser/preferences/zen-preferences.ftl b/locales/en-US/browser/browser/preferences/zen-preferences.ftl
index c803f7304..6ec2bcf30 100644
--- a/locales/en-US/browser/browser/preferences/zen-preferences.ftl
+++ b/locales/en-US/browser/browser/preferences/zen-preferences.ftl
@@ -321,6 +321,7 @@ zen-workspace-shortcut-switch-9 = Switch to Workspace 9
zen-workspace-shortcut-switch-10 = Switch to Workspace 10
zen-workspace-shortcut-forward = Forward Workspace
zen-workspace-shortcut-backward = Backward Workspace
+zen-workspace-shortcut-create = Create New Workspace
zen-sidebar-shortcut-toggle = Toggle Sidebar's Width
zen-pinned-tab-shortcut-reset = Reset Pinned Tab to Pinned URL
zen-split-view-shortcut-grid = Toggle Split View Grid
diff --git a/package-lock.json b/package-lock.json
index ed796cc6f..c0703a8f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1005,13 +1005,13 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
- "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
+ "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.11",
+ "follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
diff --git a/package.json b/package.json
index 705e3e515..96f21ace8 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"surfer": "surfer",
"test": "python3 scripts/run_tests.py",
"test:dbg": "python3 scripts/run_tests.py --jsdebugger --debug-on-failure",
- "ffprefs": "cd tools/ffprefs && cargo run --bin ffprefs -- ../../",
+ "ffprefs": "${CARGO:-cargo} run --manifest-path tools/ffprefs/Cargo.toml --bin ffprefs -- prefs engine",
"lc": "surfer license-check",
"lc:fix": "surfer license-check --fix",
"use-moz-src": "cd engine && ./mach use-moz-src",
diff --git a/prefs/zen/live-folders.yaml b/prefs/zen/live-folders.yaml
new file mode 100644
index 000000000..f6573e637
--- /dev/null
+++ b/prefs/zen/live-folders.yaml
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+- name: zen.live-folders.github.skip-new-pr-ui-check
+ value: false
diff --git a/prefs/zen/split-view.yaml b/prefs/zen/split-view.yaml
index 9448fa486..094d3516f 100644
--- a/prefs/zen/split-view.yaml
+++ b/prefs/zen/split-view.yaml
@@ -5,6 +5,9 @@
- name: zen.splitView.enable-tab-drop
value: true
+- name: zen.splitView.enable-tab-click-split
+ value: true
+
- name: zen.splitView.min-resize-width
value: 7
diff --git a/prefs/zen/theme.yaml b/prefs/zen/theme.yaml
index a77cff584..fe0a8d485 100644
--- a/prefs/zen/theme.yaml
+++ b/prefs/zen/theme.yaml
@@ -18,7 +18,7 @@
value: true
- name: zen.theme.acrylic-elements
- value: false
+ value: "@IS_TWILIGHT@"
- name: zen.theme.disable-lightweight
value: true
diff --git a/prefs/zen/view.yaml b/prefs/zen/view.yaml
index f5a54e27c..4e84831c8 100644
--- a/prefs/zen/view.yaml
+++ b/prefs/zen/view.yaml
@@ -61,5 +61,8 @@
- name: zen.view.overflow-webext-toolbar
value: "@IS_TWILIGHT@"
+- name: zen.view.overflow-webext-toolbar-threshold
+ value: 60
+
- name: zen.view.enable-loading-indicator
value: true
diff --git a/prefs/zen/workspaces.yaml b/prefs/zen/workspaces.yaml
index 58a0499ac..aeddd6802 100644
--- a/prefs/zen/workspaces.yaml
+++ b/prefs/zen/workspaces.yaml
@@ -17,6 +17,12 @@
- name: zen.workspaces.swipe-actions
value: true
+- name: zen.workspaces.swipe-actions.delta-multiplier
+ value: 90
+
+- name: zen.workspaces.switch-animation-duration
+ value: 200
+
- name: zen.workspaces.wrap-around-navigation
value: true
diff --git a/prefs/zen/zen-urlbar.yaml b/prefs/zen/zen-urlbar.yaml
index a39a77e24..fefa24e40 100644
--- a/prefs/zen/zen-urlbar.yaml
+++ b/prefs/zen/zen-urlbar.yaml
@@ -39,3 +39,6 @@
- name: zen.urlbar.suggestions.quick-actions
value: true
+
+- name: browser.urlbar.shortcuts.workspaces
+ value: true
\ No newline at end of file
diff --git a/scripts/update_service_dumps.py b/scripts/update_service_dumps.py
index 63ef85c34..1824a2660 100644
--- a/scripts/update_service_dumps.py
+++ b/scripts/update_service_dumps.py
@@ -60,4 +60,7 @@ def main():
if __name__ == "__main__":
+ import sys
+ if len(sys.argv) == 3:
+ _, DUMPS_FOLDER, ENGINE_DUMPS_FOLDER = sys.argv
main()
diff --git a/src/browser/base/content/navigator-toolbox-inc-xhtml.patch b/src/browser/base/content/navigator-toolbox-inc-xhtml.patch
index 2ee59a7b6..604d54485 100644
--- a/src/browser/base/content/navigator-toolbox-inc-xhtml.patch
+++ b/src/browser/base/content/navigator-toolbox-inc-xhtml.patch
@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
-index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..d2c6c8c150e732b77d65420520ca4905a9d3ea4d 100644
+index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..99210f8bb5633d50d2cba24f1e13ca866c5b6959 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -2,7 +2,7 @@
@@ -11,7 +11,7 @@ index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..d2c6c8c150e732b77d65420520ca4905
-@@ -18,9 +18,12 @@
+@@ -18,9 +18,13 @@
#include browser-menubar.inc
@@ -22,14 +22,14 @@ index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..d2c6c8c150e732b77d65420520ca4905
+
+
+
++
-+
+
+
diff --git a/src/browser/components/customizableui/CustomizableUI-sys-mjs.patch b/src/browser/components/customizableui/CustomizableUI-sys-mjs.patch
index 7e4c06455..5ad20a2b4 100644
--- a/src/browser/components/customizableui/CustomizableUI-sys-mjs.patch
+++ b/src/browser/components/customizableui/CustomizableUI-sys-mjs.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/customizableui/CustomizableUI.sys.mjs b/browser/components/customizableui/CustomizableUI.sys.mjs
-index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f535e9d9e 100644
+index db617c65b89643015d91b0f6a20eab7d7a1b598f..53598363bfbc2c5de4c6b4de712d22d89ee6ce1d 100644
--- a/browser/components/customizableui/CustomizableUI.sys.mjs
+++ b/browser/components/customizableui/CustomizableUI.sys.mjs
@@ -14,6 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
@@ -186,11 +186,34 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
});
lazy.log.debug(
-@@ -7933,7 +7934,14 @@ class OverflowableToolbar {
+@@ -7933,7 +7934,37 @@ class OverflowableToolbar {
Math.max(targetWidth, targetChildrenWidth)
);
totalAvailWidth = Math.ceil(totalAvailWidth);
- let isOverflowing = targetContentWidth > totalAvailWidth;
++ if (this.#target.id == 'nav-bar-customization-target' &&
++ win.gZenVerticalTabsManager._hasSetSingleToolbar &&
++ Services.prefs.getBoolPref("zen.view.overflow-webext-toolbar", true) &&
++ !win.gURLBar.hasAttribute("breakout-extend")) {
++ const availSelectors = ":is(#page-action-buttons, #zen-copy-url-button, .unified-extensions-item)";
++ const width = [
++ ...win.gURLBar._inputContainer.querySelectorAll(availSelectors),
++ ...win.document.getElementById("zen-overflow-extensions-list").querySelectorAll(availSelectors)
++ ].length * 26;
++ const urlbarWidth = win.document.getElementById("urlbar-container").getBoundingClientRect().width;
++ let overflowing = width > urlbarWidth * (Services.prefs.getIntPref("zen.view.overflow-webext-toolbar-threshold", 60) / 100);
++ let wasOverflowing = win.gURLBar._isOverflowingItems;
++ win.gURLBar._isOverflowingItems = overflowing;
++ if (wasOverflowing !== overflowing) {
++ const items = gPlacements.get("nav-bar");
++ for (let item of items) {
++ let [, node] = CustomizableUIInternal.getWidgetNode(item, win);
++ if (node?.hasAttribute("data-extensionid")) {
++ win.gZenVerticalTabsManager.appendCustomizableItem(win.document.getElementById("zen-sidebar-top-buttons-customization-target"), node);
++ }
++ }
++ }
++ }
+ if (win.gZenVerticalTabsManager._hasSetSingleToolbar && this.#toolbar.id == 'nav-bar') return { isOverflowing: false, targetContentWidth, totalAvailWidth };
+ let isOverflowing = targetContentWidth + (win.gZenVerticalTabsManager._hasSetSingleToolbar ? 0.1 : 0) > totalAvailWidth;
+ if (win.gZenVerticalTabsManager._hasSetSingleToolbar &&
@@ -202,7 +225,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
return { isOverflowing, targetContentWidth, totalAvailWidth };
}
-@@ -7994,7 +8002,11 @@ class OverflowableToolbar {
+@@ -7994,7 +8025,11 @@ class OverflowableToolbar {
return;
}
}
@@ -215,7 +238,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
lazy.log.debug(
`Need ${minSize} but width is ${totalAvailWidth} so bailing`
);
-@@ -8027,7 +8039,7 @@ class OverflowableToolbar {
+@@ -8027,7 +8062,7 @@ class OverflowableToolbar {
}
}
if (!inserted) {
@@ -224,7 +247,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
}
child.removeAttribute("cui-anchorid");
child.removeAttribute("overflowedItem");
-@@ -8153,6 +8165,9 @@ class OverflowableToolbar {
+@@ -8153,6 +8188,9 @@ class OverflowableToolbar {
* if no such list exists.
*/
get #webExtList() {
@@ -234,7 +257,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
if (!this.#webExtListRef) {
let targetID = this.#toolbar.getAttribute("addon-webext-overflowtarget");
if (!targetID) {
-@@ -8164,6 +8179,9 @@ class OverflowableToolbar {
+@@ -8164,6 +8202,9 @@ class OverflowableToolbar {
let win = this.#toolbar.ownerGlobal;
let { panel } = win.gUnifiedExtensions;
this.#webExtListRef = panel.querySelector(`#${targetID}`);
@@ -244,7 +267,7 @@ index db617c65b89643015d91b0f6a20eab7d7a1b598f..9acef640800bdc75f477a8e14e73f08f
}
return this.#webExtListRef;
}
-@@ -8372,7 +8390,7 @@ class OverflowableToolbar {
+@@ -8372,7 +8413,7 @@ class OverflowableToolbar {
break;
}
case "mousedown": {
diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch
index 5cf6a04fb..262f69623 100644
--- a/src/browser/components/tabbrowser/content/tab-js.patch
+++ b/src/browser/components/tabbrowser/content/tab-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
-index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..139fa9be7919928e5a57fda6c7fabe4bc5acf982 100644
+index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..fb9ec4deb5871bc0ba57c323a413f07440e9aa42 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -151,14 +151,15 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..139fa9be7919928e5a57fda6c7fabe4b
on_click(event) {
if (event.button != 0) {
return;
-@@ -617,14 +656,30 @@
+@@ -617,14 +656,31 @@
trigger: "alt_click",
});
}
+ if (
+ !event.target.classList.contains("tab-close-button") &&
+ !event.target.classList.contains("tab-icon-overlay") &&
-+ !event.target.classList.contains("tab-audio-button")
++ !event.target.classList.contains("tab-audio-button") &&
++ Services.prefs.getBoolPref("zen.splitView.enable-tab-click-split", false)
+ ) {
+ if (!this.splitView) {
+ gZenViewSplitter.contextSplitTabs(this);
@@ -183,7 +184,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..139fa9be7919928e5a57fda6c7fabe4b
gBrowser.multiSelectedTabsCount > 0 &&
!event.target.classList.contains("tab-close-button") &&
!event.target.classList.contains("tab-icon-overlay") &&
-@@ -636,8 +691,9 @@
+@@ -636,8 +692,9 @@
}
if (
@@ -195,7 +196,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..139fa9be7919928e5a57fda6c7fabe4b
) {
if (this.activeMediaBlocked) {
if (this.multiselected) {
-@@ -655,7 +711,7 @@
+@@ -655,7 +712,7 @@
return;
}
@@ -204,7 +205,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..139fa9be7919928e5a57fda6c7fabe4b
if (this.multiselected) {
gBrowser.removeMultiSelectedTabs(
lazy.TabMetrics.userTriggeredContext(
-@@ -675,6 +731,14 @@
+@@ -675,6 +732,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -219,7 +220,7 @@ index 2e02bad1a7c89b4c3b5aee1e14c13bb953a64eb6..139fa9be7919928e5a57fda6c7fabe4b
}
on_dblclick(event) {
-@@ -698,6 +762,8 @@
+@@ -698,6 +763,8 @@
animate: true,
triggeringEvent: event,
});
diff --git a/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch b/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch
index 1a27bcc25..9b41ab880 100644
--- a/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch
+++ b/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch
@@ -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,
diff --git a/src/browser/components/urlbar/UrlbarProvidersManager-sys-mjs.patch b/src/browser/components/urlbar/UrlbarProvidersManager-sys-mjs.patch
index 1993cacce..92a550307 100644
--- a/src/browser/components/urlbar/UrlbarProvidersManager-sys-mjs.patch
+++ b/src/browser/components/urlbar/UrlbarProvidersManager-sys-mjs.patch
@@ -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:
diff --git a/src/browser/components/urlbar/UrlbarTokenizer-sys-mjs.patch b/src/browser/components/urlbar/UrlbarTokenizer-sys-mjs.patch
new file mode 100644
index 000000000..c195ba7d9
--- /dev/null
+++ b/src/browser/components/urlbar/UrlbarTokenizer-sys-mjs.patch
@@ -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);
+ },
+
diff --git a/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch
index 4082b9b91..a4c5e9b40 100644
--- a/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch
+++ b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch
@@ -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..92f91379f43785cf5417c96b178884c860db6bd3 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-search-mode-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":
diff --git a/src/browser/components/urlbar/content/enUS-searchFeatures-ftl.patch b/src/browser/components/urlbar/content/enUS-searchFeatures-ftl.patch
new file mode 100644
index 000000000..d041e4155
--- /dev/null
+++ b/src/browser/components/urlbar/content/enUS-searchFeatures-ftl.patch
@@ -0,0 +1,14 @@
+diff --git a/browser/components/urlbar/content/enUS-searchFeatures.ftl b/browser/components/urlbar/content/enUS-searchFeatures.ftl
+index a1fb86058d1f6f015160163f75999b4a429bf1fd..83098291e21a8239513a4105a4604394a1dfabc0 100644
+--- a/browser/components/urlbar/content/enUS-searchFeatures.ftl
++++ b/browser/components/urlbar/content/enUS-searchFeatures.ftl
+@@ -2,6 +2,9 @@
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
++urlbar-search-mode-workspaces = Spaces
++urlbar-search-mode-workspaces-en = Spaces
++
+ ### These strings are related to the Firefox Suggest feature. Firefox Suggest
+ ### shows recommended and sponsored third-party results in the address bar
+ ### panel. It also shows headings/labels above different groups of results. For
diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css
index 805d66d95..0e949942f 100644
--- a/src/browser/themes/shared/zen-icons/icons.css
+++ b/src/browser/themes/shared/zen-icons/icons.css
@@ -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;
}
diff --git a/src/external-patches/firefox/allow_backdrop_to_work_on_transparency.patch b/src/external-patches/firefox/allow_backdrop_to_work_on_transparency.patch
new file mode 100644
index 000000000..befed26bc
--- /dev/null
+++ b/src/external-patches/firefox/allow_backdrop_to_work_on_transparency.patch
@@ -0,0 +1,172 @@
+diff --git a/gfx/webrender_bindings/WebRenderAPI.cpp b/gfx/webrender_bindings/WebRenderAPI.cpp
+--- a/gfx/webrender_bindings/WebRenderAPI.cpp
++++ b/gfx/webrender_bindings/WebRenderAPI.cpp
+@@ -298,11 +298,13 @@
+ panic_on_gl_error, picTileWidth, picTileHeight,
+ gfx::gfxVars::WebRenderRequiresHardwareDriver(),
+ StaticPrefs::gfx_webrender_low_quality_pinch_zoom_AtStartup(),
+ StaticPrefs::gfx_webrender_max_shared_surface_size_AtStartup(),
+ StaticPrefs::gfx_webrender_enable_subpixel_aa_AtStartup(),
+- compositor->ShouldUseLayerCompositor())) {
++ compositor->ShouldUseLayerCompositor(),
++ StaticPrefs::
++ gfx_webrender_opaque_backdrop_fallback_AtStartup())) {
+ // wr_window_new puts a message into gfxCriticalNote if it returns
+ // false
+ MOZ_ASSERT(errorMessage);
+ error.AssignASCII(errorMessage);
+ wr_api_free_error_msg(errorMessage);
+diff --git a/gfx/webrender_bindings/src/bindings.rs b/gfx/webrender_bindings/src/bindings.rs
+--- a/gfx/webrender_bindings/src/bindings.rs
++++ b/gfx/webrender_bindings/src/bindings.rs
+@@ -1998,10 +1998,11 @@
+ reject_software_rasterizer: bool,
+ low_quality_pinch_zoom: bool,
+ max_shared_surface_size: i32,
+ enable_subpixel_aa: bool,
+ use_layer_compositor: bool,
++ opaque_backdrop_fallback: bool,
+ ) -> bool {
+ assert!(unsafe { is_in_render_thread() });
+
+ // Ensure the WR profiler callbacks are hooked up to the Gecko profiler.
+ set_profiler_hooks(Some(&PROFILER_HOOKS));
+@@ -2164,10 +2165,11 @@
+ texture_cache_config,
+ reject_software_rasterizer,
+ low_quality_pinch_zoom,
+ max_shared_surface_size,
+ enable_dithering,
++ opaque_backdrop_fallback,
+ precise_linear_gradients,
+ ..Default::default()
+ };
+
+ let window_size = DeviceIntSize::new(window_width, window_height);
+diff --git a/gfx/wr/webrender/src/device/gl.rs b/gfx/wr/webrender/src/device/gl.rs
+--- a/gfx/wr/webrender/src/device/gl.rs
++++ b/gfx/wr/webrender/src/device/gl.rs
+@@ -3982,10 +3982,14 @@
+
+ pub fn disable_color_write(&self) {
+ self.gl.color_mask(false, false, false, false);
+ }
+
++ pub fn set_color_mask(&self, r: bool, g: bool, b: bool, a: bool) {
++ self.gl.color_mask(r, g, b, a);
++ }
++
+ pub fn set_blend(&mut self, enable: bool) {
+ if enable {
+ self.gl.enable(gl::BLEND);
+ } else {
+ self.gl.disable(gl::BLEND);
+diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/renderer/init.rs
+--- a/gfx/wr/webrender/src/renderer/init.rs
++++ b/gfx/wr/webrender/src/renderer/init.rs
+@@ -204,10 +204,12 @@
+ pub low_quality_pinch_zoom: bool,
+ pub max_shared_surface_size: i32,
+ /// If true, open a debug socket to listen for remote debugger.
+ /// Relies on `debugger` cargo feature being enabled.
+ pub enable_debugger: bool,
++ /// See explanation of `gfx.webrender.opaque-backdrop-fallback`.
++ pub opaque_backdrop_fallback: bool,
+
+ /// Use a more precise method for sampling gradients.
+ pub precise_linear_gradients: bool,
+ }
+
+@@ -277,10 +279,11 @@
+ enable_instancing: true,
+ reject_software_rasterizer: false,
+ low_quality_pinch_zoom: false,
+ max_shared_surface_size: 2048,
+ enable_debugger: true,
++ opaque_backdrop_fallback: false,
+ precise_linear_gradients: false,
+ }
+ }
+ }
+
+@@ -802,10 +805,11 @@
+ allocated_native_surfaces: FastHashSet::default(),
+ debug_overlay_state: DebugOverlayState::new(),
+ buffer_damage_tracker: BufferDamageTracker::default(),
+ max_primitive_instance_count,
+ enable_instancing: options.enable_instancing,
++ opaque_backdrop_fallback: options.opaque_backdrop_fallback,
+ consecutive_oom_frames: 0,
+ target_frame_publish_id: None,
+ pending_result_msg: None,
+ layer_compositor_frame_state_in_prev_frame: None,
+ external_composite_debug_items: Vec::new(),
+diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
+--- a/gfx/wr/webrender/src/renderer/mod.rs
++++ b/gfx/wr/webrender/src/renderer/mod.rs
+@@ -867,10 +867,12 @@
+ buffer_damage_tracker: BufferDamageTracker,
+
+ max_primitive_instance_count: usize,
+ enable_instancing: bool,
+
++ opaque_backdrop_fallback: bool,
++
+ /// Count consecutive oom frames to detectif we are stuck unable to render
+ /// in a loop.
+ consecutive_oom_frames: u32,
+
+ /// update() defers processing of ResultMsg, if frame_publish_id of
+@@ -2787,18 +2789,29 @@
+ let read_target = ReadTarget::from_texture(cache_texture);
+
+ // Should always be drawing to picture cache tiles or off-screen surface!
+ debug_assert!(!draw_target.is_default());
+ let device_to_framebuffer = Scale::new(1i32);
++ let dest_fb_rect = dest * device_to_framebuffer;
+
+ self.device.blit_render_target(
+ read_target,
+ src * device_to_framebuffer,
+ draw_target,
+- dest * device_to_framebuffer,
++ dest_fb_rect,
+ TextureFilter::Linear,
+ );
++
++ if self.opaque_backdrop_fallback {
++ self.device.set_color_mask(false, false, false, true);
++ self.device.clear_target(
++ Some([0.0, 0.0, 0.0, 1.0]),
++ None,
++ Some(dest_fb_rect),
++ );
++ self.device.set_color_mask(true, true, true, true);
++ }
+ }
+ }
+ }
+
+ fn draw_picture_cache_target(
+diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
+--- a/modules/libpref/init/StaticPrefList.yaml
++++ b/modules/libpref/init/StaticPrefList.yaml
+@@ -8439,10 +8439,17 @@
+ #else
+ value: false
+ #endif
+ mirror: once
+
++# Make backdrop-filter treat its captured backdrop as if it had been
++# composited over an opaque-black background. (See bug 2036640)
++- name: gfx.webrender.opaque-backdrop-fallback
++ type: bool
++ value: true
++ mirror: once
++
+ # Disable wait of GPU execution completion
+ - name: gfx.webrender.wait-gpu-finished.disabled
+ type: bool
+ value: false
+ mirror: once
+
diff --git a/src/external-patches/firefox/native_macos_popovers.patch b/src/external-patches/firefox/native_macos_popovers.patch
index 8c23d2d7d..9029cc846 100644
--- a/src/external-patches/firefox/native_macos_popovers.patch
+++ b/src/external-patches/firefox/native_macos_popovers.patch
@@ -1,7 +1,7 @@
diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml
--- a/browser/base/content/main-popupset.inc.xhtml
+++ b/browser/base/content/main-popupset.inc.xhtml
-@@ -196,10 +196,11 @@
+@@ -192,10 +192,11 @@
-@@ -207,10 +208,11 @@
+@@ -203,10 +204,11 @@
-@@ -560,10 +562,11 @@
+@@ -610,10 +612,11 @@
type="arrow"
orient="vertical"
noautofocus="true"
@@ -40,7 +40,7 @@ diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content
diff --git a/browser/components/asrouter/modules/FeatureCallout.sys.mjs b/browser/components/asrouter/modules/FeatureCallout.sys.mjs
--- a/browser/components/asrouter/modules/FeatureCallout.sys.mjs
+++ b/browser/components/asrouter/modules/FeatureCallout.sys.mjs
-@@ -1046,10 +1046,11 @@
+@@ -1054,10 +1054,11 @@
noautofocus="true"
flip="slide"
type="arrow"
@@ -70,22 +70,22 @@ diff --git a/browser/components/customizableui/content/panelUI.inc.xhtml b/brows
diff --git a/dom/xul/XULPopupElement.cpp b/dom/xul/XULPopupElement.cpp
--- a/dom/xul/XULPopupElement.cpp
+++ b/dom/xul/XULPopupElement.cpp
-@@ -82,10 +82,14 @@
+@@ -80,10 +80,14 @@
+ }
void XULPopupElement::OpenPopupAtScreen(int32_t aXPos, int32_t aYPos,
bool aIsContextMenu,
Event* aTriggerEvent) {
- nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ // TODO(cheff): At nsCocoaWindow::Show but we check for ShouldShowAsNSPopover
+ // to determine whether to use a native popover or not. This should sort of
+ // "replicate" that logic here, but it's a bit of a hacky way.
+ SetAttr(kNameSpaceID_None, nsGkAtoms::nonnativepopover, u"true"_ns, true);
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (pm) {
pm->ShowPopupAtScreen(this, aXPos, aYPos, aIsContextMenu, aTriggerEvent);
}
}
-
-@@ -94,10 +98,14 @@
+@@ -93,10 +97,14 @@
int32_t aWidth, int32_t aHeight,
bool aIsContextMenu,
bool aAttributesOverride,
@@ -103,7 +103,7 @@ diff --git a/dom/xul/XULPopupElement.cpp b/dom/xul/XULPopupElement.cpp
diff --git a/layout/xul/nsMenuPopupFrame.h b/layout/xul/nsMenuPopupFrame.h
--- a/layout/xul/nsMenuPopupFrame.h
+++ b/layout/xul/nsMenuPopupFrame.h
-@@ -530,18 +530,10 @@
+@@ -528,18 +528,10 @@
// Move the popup to the position specified in its |left| and |top|
// attributes.
@@ -122,7 +122,7 @@ diff --git a/layout/xul/nsMenuPopupFrame.h b/layout/xul/nsMenuPopupFrame.h
public:
/**
* Return whether the popup direction should be RTL.
-@@ -550,10 +542,18 @@
+@@ -548,10 +540,18 @@
*
* Return whether the popup direction should be RTL.
*/
@@ -144,7 +144,7 @@ diff --git a/layout/xul/nsMenuPopupFrame.h b/layout/xul/nsMenuPopupFrame.h
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
-@@ -19477,10 +19477,19 @@
+@@ -19672,10 +19672,19 @@
value: true
mirror: always
@@ -167,7 +167,7 @@ diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/Sta
diff --git a/toolkit/themes/shared/global-shared.css b/toolkit/themes/shared/global-shared.css
--- a/toolkit/themes/shared/global-shared.css
+++ b/toolkit/themes/shared/global-shared.css
-@@ -85,10 +85,22 @@
+@@ -72,10 +72,22 @@
--menuitem-border-radius: var(--arrowpanel-menuitem-border-radius);
--menuitem-padding: var(--arrowpanel-menuitem-padding);
--menuitem-margin: var(--arrowpanel-menuitem-margin);
@@ -251,7 +251,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
-@@ -3,10 +3,13 @@
+@@ -4,10 +4,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsCocoaWindow.h"
@@ -265,7 +265,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
#include "nsIDOMWindowUtils.h"
#include "nsILocalFileMac.h"
#include "CocoaCompositorWidget.h"
-@@ -4973,10 +4976,15 @@
+@@ -5031,10 +5034,15 @@
if (mWindowType == WindowType::Popup) {
SetPopupWindowLevel();
mWindow.backgroundColor = NSColor.clearColor;
@@ -281,7 +281,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
// the active space. Does not work with multiple displays. See
// NeedsRecreateToReshow() for multi-display with multi-space workaround.
mWindow.collectionBehavior = mWindow.collectionBehavior |
-@@ -5178,10 +5186,57 @@
+@@ -5236,10 +5244,57 @@
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
@@ -339,54 +339,58 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
if (!mWindow) {
-@@ -5242,10 +5297,54 @@
+@@ -5300,10 +5355,58 @@
mWindow.contentView.needsDisplay = YES;
if (!nativeParentWindow || mPopupLevel != PopupLevel::Parent) {
[mWindow orderFront:nil];
}
NS_OBJC_END_TRY_IGNORE_BLOCK;
-+ if (ShouldShowAsNSPopover()) {
++ if (ShouldShowAsNSPopover() && nativeParentWindow) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrame();
-+ if (nativeParentWindow) {
-+ NSRectEdge preferredEdge =
-+ AlignmentPositionToNSRectEdge(popupFrame->GetAlignmentPosition());
-+ nsRect anchorRectAppUnits = popupFrame->GetUntransformedAnchorRect();
-+ nsPresContext* pc = popupFrame->PresContext();
-+ int32_t appUnitsPerDevPixel = pc->AppUnitsPerDevPixel();
-+ mozilla::DesktopToLayoutDeviceScale desktopToLayoutScale =
-+ pc->DeviceContext()->GetDesktopToDeviceScale();
-+ mozilla::DesktopIntRect popupAnchorRectScaled =
-+ mozilla::DesktopIntRect::RoundOut(
-+ mozilla::LayoutDeviceRect::FromAppUnits(anchorRectAppUnits,
-+ appUnitsPerDevPixel) /
-+ desktopToLayoutScale);
-+ // Taking the now correctly scaled anchor rect and turning it into a
-+ // gecko rect this accounts for the y-axis inversion that cocoa needs,
-+ // as the origin is in the bottom left. This rect is in screen space
-+ NSRect cocoaScreenRect =
-+ nsCocoaUtils::GeckoRectToCocoaRect(popupAnchorRectScaled);
-+ // We take the screen space rect and convert it to window space
-+ // coordinates, as NSPopover requires the coordinates to be in view
-+ // space and inside the view. If the coordinates are outside our view,
-+ // the popover will fail silently
-+ NSRect windowRect =
-+ [nativeParentWindow convertRectFromScreen:cocoaScreenRect];
-+ NSView* parentView = [nativeParentWindow contentView];
-+ // We take the window space rect and convert it to view space for the
-+ // specific parent view
-+ NSRect positioningRect = [parentView convertRect:windowRect
-+ fromView:nil];
-+ BOOL shouldHideAnchor = NO;
-+ auto& element = popupFrame->PopupElement();
-+ if (element.GetBoolAttr(nsGkAtoms::hidepopovertail)) {
-+ shouldHideAnchor = YES;
-+ }
-+ [(PopupWindow*)mWindow showPopoverRelativeToRect:positioningRect
-+ ofView:parentView
-+ preferredEdge:preferredEdge
-+ hiddenAnchor:shouldHideAnchor];
-+ SyncPopoverBounds([(PopupWindow*)mWindow popover], popupFrame);
++ NSRectEdge preferredEdge =
++ AlignmentPositionToNSRectEdge(popupFrame->GetAlignmentPosition());
++ nsRect anchorRectAppUnits = popupFrame->GetUntransformedAnchorRect();
++ nsPresContext* pc = popupFrame->PresContext();
++ int32_t appUnitsPerDevPixel = pc->AppUnitsPerDevPixel();
++ mozilla::DesktopToLayoutDeviceScale desktopToLayoutScale =
++ pc->DeviceContext()->GetDesktopToDeviceScale();
++ mozilla::DesktopIntRect popupAnchorRectScaled =
++ mozilla::DesktopIntRect::RoundOut(
++ mozilla::LayoutDeviceRect::FromAppUnits(anchorRectAppUnits,
++ appUnitsPerDevPixel) /
++ desktopToLayoutScale);
++ // Taking the now correctly scaled anchor rect and turning it into a
++ // gecko rect this accounts for the y-axis inversion that cocoa needs,
++ // as the origin is in the bottom left. This rect is in screen space
++ NSRect cocoaScreenRect =
++ nsCocoaUtils::GeckoRectToCocoaRect(popupAnchorRectScaled);
++ // We take the screen space rect and convert it to window space
++ // coordinates, as NSPopover requires the coordinates to be in view
++ // space and inside the view. If the coordinates are outside our view,
++ // the popover will fail silently
++ NSRect windowRect =
++ [nativeParentWindow convertRectFromScreen:cocoaScreenRect];
++ NSView* parentView = [nativeParentWindow contentView];
++ // We take the window space rect and convert it to view space for the
++ // specific parent view
++ NSRect positioningRect = [parentView convertRect:windowRect
++ fromView:nil];
++ BOOL shouldHideAnchor = NO;
++ auto& element = popupFrame->PopupElement();
++ if (element.GetBoolAttr(nsGkAtoms::hidepopovertail)) {
++ shouldHideAnchor = YES;
+ }
++ [(PopupWindow*)mWindow showPopoverRelativeToRect:positioningRect
++ ofView:parentView
++ preferredEdge:preferredEdge
++ hiddenAnchor:shouldHideAnchor];
++ SyncPopoverBounds([(PopupWindow*)mWindow popover], popupFrame);
++ if (mPopupLevel == PopupLevel::Parent) {
++ [nativeParentWindow addChildWindow:mWindow ordered:NSWindowAbove];
++ }
++
++ // Exit early here since the popover is now shown.
++ mWindow.isBeingShown = NO;
+ return;
+ }
// If our popup window is a non-native context menu, tell the OS (and
@@ -394,12 +398,13 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
// close other programs' context menus when ours open.
if ([mWindow isKindOfClass:[PopupWindow class]] &&
[(PopupWindow*)mWindow isContextMenu]) {
-@@ -5316,10 +5415,15 @@
+@@ -5373,11 +5476,15 @@
+ // unhook it here before ordering it out. When you order out the child
// of a window it hides the parent window.
if (mWindowType == WindowType::Popup && nativeParentWindow) {
[nativeParentWindow removeChildWindow:mWindow];
}
-
+-
+ // Handle NSPopover hiding or traditional window hiding
+ if ([mWindow isKindOfClass:[PopupWindow class]] &&
+ [(PopupWindow*)mWindow usePopover]) {
@@ -410,7 +415,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
// other programs) that a menu has closed.
if ([mWindow isKindOfClass:[PopupWindow class]] &&
[(PopupWindow*)mWindow isContextMenu]) {
-@@ -5366,10 +5470,28 @@
+@@ -5424,10 +5531,28 @@
return false;
}
return nsIWidget::ShouldUseOffMainThreadCompositing();
@@ -439,12 +444,12 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
return mWindow.isOpaque ? TransparencyMode::Opaque
: TransparencyMode::Transparent;
-@@ -6328,10 +6450,20 @@
+@@ -6378,10 +6503,19 @@
+
// We ignore aRepaint -- we have to call display:YES, otherwise the
// title bar doesn't immediately get repainted and is displayed in
// the wrong place, leading to a visual jump.
[mWindow setFrame:newFrame display:YES];
-
+ if (ShouldUseNSPopover() && [(PopupWindow*)mWindow usePopover]) {
+ [(PopupWindow*)mWindow updatePopoverContent];
+ // A popover won't resize by setting the frame
@@ -454,18 +459,18 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
+ [[(PopupWindow*)mWindow popover] setContentSize:contentSize];
+ SyncPopoverBounds([(PopupWindow*)mWindow popover], GetPopupFrame());
+ }
-+
+
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
void nsCocoaWindow::Resize(const DesktopRect& aRect, bool aRepaint) {
- DoResize(aRect.x, aRect.y, aRect.width, aRect.height, aRepaint, false);
-@@ -8314,17 +8446,26 @@
+@@ -8393,18 +8527,31 @@
+ backing:bufferingType
defer:deferCreation];
if (!self) {
return nil;
}
-
+-
+ mPopover = nil;
+ mPopoverViewController = nil;
+ mUsePopover = NO;
@@ -477,6 +482,11 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
}
+- (void)dealloc {
++ if (mPopover) {
++ ChildViewMouseTracker::OnDestroyWindow(
++ mPopover.contentViewController.view.window);
++ }
++
+ [mPopover release];
+ [mPopoverViewController release];
+ [super dealloc];
@@ -487,7 +497,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
// Return 0 in order to match what the system does for sheet windows and
// _NSPopoverWindows.
- (CGFloat)_backdropBleedAmount {
-@@ -8378,10 +8519,122 @@
+@@ -8460,10 +8607,122 @@
- (void)setIsContextMenu:(BOOL)flag {
mIsContextMenu = flag;
@@ -613,7 +623,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
diff --git a/widget/nsIWidget.h b/widget/nsIWidget.h
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
-@@ -843,10 +843,15 @@
+@@ -829,10 +829,15 @@
virtual void SuppressAnimation(bool aSuppress) {}
/** Sets windows-specific mica backdrop on this widget. */
@@ -644,7 +654,7 @@ diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py
Atom("highest", "highest"),
Atom("horizontal", "horizontal"),
Atom("hover", "hover"),
-@@ -757,10 +758,11 @@
+@@ -759,10 +760,11 @@
Atom("nohref", "nohref"),
Atom("noinitialselection", "noinitialselection"),
Atom("nomodule", "nomodule"),
diff --git a/src/external-patches/firefox/session_store_use_size_hint/D247215.patch b/src/external-patches/firefox/session_store_use_size_hint/D247215.patch
deleted file mode 100644
index 53e619863..000000000
--- a/src/external-patches/firefox/session_store_use_size_hint/D247215.patch
+++ /dev/null
@@ -1,251 +0,0 @@
-diff --git a/dom/chrome-webidl/IOUtils.webidl b/dom/chrome-webidl/IOUtils.webidl
---- a/dom/chrome-webidl/IOUtils.webidl
-+++ b/dom/chrome-webidl/IOUtils.webidl
-@@ -94,23 +94,23 @@
- * otherwise rejects with a DOMException.
- */
- [NewObject]
- Promise writeUTF8(DOMString path, UTF8String string, optional WriteOptions options = {});
- /**
-- * Attempts to serialize |value| into a JSON string and encode it as into a
-- * UTF-8 string, then safely write the result to a file at |path|. Works
-- * exactly like |write|.
-+ * Attempts to serialize |value| into a JSON string and encode it as a UTF-8
-+ * string, then safely write the result to a file at |path|. Works exactly
-+ * like |write|.
- *
- * @param path An absolute file path
- * @param value The value to be serialized.
- * @param options Options for writing the file. The "append" mode is not supported.
- *
- * @return Resolves with the number of bytes successfully written to the file,
- * otherwise rejects with a DOMException.
- */
- [NewObject]
-- Promise writeJSON(DOMString path, any value, optional WriteOptions options = {});
-+ Promise writeJSON(DOMString path, any value, optional WriteJSONOptions options = {});
- /**
- * Moves the file from |sourcePath| to |destPath|, creating necessary parents.
- * If |destPath| is a directory, then the source file will be moved into the
- * destination directory.
- *
-@@ -567,10 +567,39 @@
- * If true, compress the data with LZ4-encoding before writing to the file.
- */
- boolean compress = false;
- };
-
-+/**
-+ * Options to be passed to the |IOUtils.writeJSON| method.
-+ */
-+dictionary WriteJSONOptions: WriteOptions {
-+ /**
-+ * An optional length hint that will be used to pre-allocate the buffer that
-+ * will hold the stringified JSON.
-+ *
-+ * This is the *length* and not the size (i.e., it is the number of UTF-16
-+ * codepoints and not the number of bytes).
-+ */
-+ unsigned long long lengthHint = 0;
-+};
-+
-+/**
-+ * Information about a WriteJSON operation.
-+ */
-+dictionary WriteJSONResult {
-+ /**
-+ * The number of bytes written.
-+ */
-+ required unsigned long long size;
-+
-+ /**
-+ * The length of the stringified JSON (in UTF-16 codepoints).
-+ */
-+ required unsigned long long jsonLength;
-+};
-+
- /**
- * Options to be passed to the |IOUtils.move| method.
- */
- dictionary MoveOptions {
- /**
-diff --git a/xpcom/ioutils/IOUtils.h b/xpcom/ioutils/IOUtils.h
---- a/xpcom/ioutils/IOUtils.h
-+++ b/xpcom/ioutils/IOUtils.h
-@@ -94,11 +94,11 @@
- const nsACString& aString, const dom::WriteOptions& aOptions,
- ErrorResult& aError);
-
- static already_AddRefed WriteJSON(
- dom::GlobalObject& aGlobal, const nsAString& aPath,
-- JS::Handle aValue, const dom::WriteOptions& aOptions,
-+ JS::Handle aValue, const dom::WriteJSONOptions& aOptions,
- ErrorResult& aError);
-
- static already_AddRefed Move(dom::GlobalObject& aGlobal,
- const nsAString& aSourcePath,
- const nsAString& aDestPath,
-@@ -736,13 +736,16 @@
- RefPtr mBackupFile;
- RefPtr mTmpFile;
- dom::WriteMode mMode;
- bool mFlush = false;
- bool mCompress = false;
-+ size_t mLengthHint = 0;
-
- static Result FromBinding(
- const dom::WriteOptions& aOptions);
-+ static Result FromBinding(
-+ const dom::WriteJSONOptions& aOptions);
- };
-
- /**
- * Re-implements the file compression and decompression utilities found
- * in toolkit/components/lz4/lz4.js
-diff --git a/xpcom/ioutils/IOUtils.cpp b/xpcom/ioutils/IOUtils.cpp
---- a/xpcom/ioutils/IOUtils.cpp
-+++ b/xpcom/ioutils/IOUtils.cpp
-@@ -589,15 +589,21 @@
- return WriteSync(file, AsBytes(Span(str)), opts);
- });
- });
- }
-
-+static bool AppendJSON(const char16_t* aBuf, uint32_t aLen, void* aStr) {
-+ nsAString* str = static_cast(aStr);
-+
-+ return str->Append(aBuf, aLen, fallible);
-+}
-+
- /* static */
- already_AddRefed IOUtils::WriteJSON(GlobalObject& aGlobal,
- const nsAString& aPath,
- JS::Handle aValue,
-- const WriteOptions& aOptions,
-+ const WriteJSONOptions& aOptions,
- ErrorResult& aError) {
- return WithPromiseAndState(
- aGlobal, aError, [&](Promise* promise, auto& state) {
- nsCOMPtr file = new nsLocalFile();
- REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
-@@ -623,14 +629,15 @@
- file->HumanReadablePath().get()));
- return;
- }
-
- JSContext* cx = aGlobal.Context();
-- JS::Rooted rootedValue(cx, aValue);
-+ JS::Rooted value(cx, aValue);
- nsString string;
-- if (!nsContentUtils::StringifyJSON(cx, aValue, string,
-- UndefinedIsNullStringLiteral)) {
-+ if (!JS_StringifyWithLengthHint(cx, &value, nullptr,
-+ JS::NullHandleValue, AppendJSON,
-+ &string, opts.mLengthHint)) {
- JS::Rooted exn(cx, JS::UndefinedValue());
- if (JS_GetPendingException(cx, &exn)) {
- JS_ClearPendingException(cx);
- promise->MaybeReject(exn);
- } else {
-@@ -639,22 +646,29 @@
- "Could not serialize object to JSON"_ns));
- }
- return;
- }
-
-- DispatchAndResolve(
-+ DispatchAndResolve(
- state->mEventQueue, promise,
- [file = std::move(file), string = std::move(string),
-- opts = std::move(opts)]() -> Result {
-+ opts = std::move(opts)]() -> Result {
- nsAutoCString utf8Str;
- if (!CopyUTF16toUTF8(string, utf8Str, fallible)) {
- return Err(IOError(
- NS_ERROR_OUT_OF_MEMORY,
- "Failed to write to `%s': could not allocate buffer",
- file->HumanReadablePath().get()));
- }
-- return WriteSync(file, AsBytes(Span(utf8Str)), opts);
-+
-+ uint32_t size =
-+ MOZ_TRY(WriteSync(file, AsBytes(Span(utf8Str)), opts));
-+
-+ dom::WriteJSONResult result;
-+ result.mSize = size;
-+ result.mJsonLength = static_cast(string.Length());
-+ return result;
- });
- });
- }
-
- /* static */
-@@ -2840,10 +2854,20 @@
-
- opts.mCompress = aOptions.mCompress;
- return opts;
- }
-
-+Result
-+IOUtils::InternalWriteOpts::FromBinding(const WriteJSONOptions& aOptions) {
-+ InternalWriteOpts opts =
-+ MOZ_TRY(FromBinding(static_cast(aOptions)));
-+
-+ opts.mLengthHint = aOptions.mLengthHint;
-+
-+ return opts;
-+}
-+
- /* static */
- Result IOUtils::JsBuffer::Create(
- IOUtils::BufferKind aBufferKind, size_t aCapacity) {
- JsBuffer buffer(aBufferKind, aCapacity);
- if (aCapacity != 0 && !buffer.mBuffer) {
-diff --git a/xpcom/ioutils/tests/test_ioutils_read_write_json.html b/xpcom/ioutils/tests/test_ioutils_read_write_json.html
---- a/xpcom/ioutils/tests/test_ioutils_read_write_json.html
-+++ b/xpcom/ioutils/tests/test_ioutils_read_write_json.html
-@@ -140,10 +140,43 @@
- );
-
- await cleanup(filename);
- });
-
-+ add_task(async function test_writeJSON_return() {
-+ const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_writeJSON_return.tmp");
-+
-+ const obj = { emoji: "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️🌈 🥠 🏴☠️ 🪐" };
-+
-+ const expectedJson = JSON.stringify(obj);
-+ const size = new TextEncoder().encode(expectedJson).byteLength;
-+
-+ {
-+ const result = await IOUtils.writeJSON(filename, obj, { lengthHint: 0 });
-+
-+ is(await IOUtils.readUTF8(filename), expectedJson, "should have written expected JSON");
-+
-+ is(typeof result, "object", "writeJSON returns an object");
-+ ok(result !== null, "writeJSON returns non-null");
-+
-+ ok(Object.hasOwn(result, "size"), "result has size property");
-+ ok(Object.hasOwn(result, "jsonLength"), "result has jsonLength property");
-+
-+ is(result.size, size, "Should have written the expected number of bytes");
-+ is(result.jsonLength, expectedJson.length, "Should have written the expected number of UTF-16 codepoints");
-+ }
-+
-+ {
-+ const result = await IOUtils.writeJSON(filename, obj, { lengthHint: expectedJson.length, compress: true });
-+
-+ isnot(result.size, size, "Should have written a different number of bytes due to compression");
-+ is(result.jsonLength, expectedJson.length, "Should have written the same number of UTF-16 codepoints");
-+ }
-+
-+ await cleanup(filename);
-+ });
-+
- add_task(async function test_append_json() {
- const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_append_json.tmp");
-
- await IOUtils.writeJSON(filename, OBJECT);
-
-
diff --git a/src/external-patches/firefox/session_store_use_size_hint/D247217.patch b/src/external-patches/firefox/session_store_use_size_hint/D247217.patch
deleted file mode 100644
index 31ffe06f9..000000000
--- a/src/external-patches/firefox/session_store_use_size_hint/D247217.patch
+++ /dev/null
@@ -1,279 +0,0 @@
-diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
---- a/browser/components/sessionstore/SessionFile.sys.mjs
-+++ b/browser/components/sessionstore/SessionFile.sys.mjs
-@@ -503,10 +503,12 @@
- if (isFinalWrite) {
- Services.obs.notifyObservers(
- null,
- "sessionstore-final-state-write-complete"
- );
-+
-+ lazy.SessionWriter.deinit();
- }
- });
- },
-
- async wipe() {
-diff --git a/browser/components/sessionstore/SessionWriter.sys.mjs b/browser/components/sessionstore/SessionWriter.sys.mjs
---- a/browser/components/sessionstore/SessionWriter.sys.mjs
-+++ b/browser/components/sessionstore/SessionWriter.sys.mjs
-@@ -6,10 +6,12 @@
-
- ChromeUtils.defineESModuleGetters(lazy, {
- sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
- });
-
-+const BROWSER_PURGE_SESSION_HISTORY = "browser:purge-session-history";
-+
- /**
- * We just started (we haven't written anything to disk yet) from
- * `Paths.clean`. The backup directory may not exist.
- */
- const STATE_CLEAN = "clean";
-@@ -58,10 +60,14 @@
- export const SessionWriter = {
- init(origin, useOldExtension, paths, prefs = {}) {
- return SessionWriterInternal.init(origin, useOldExtension, paths, prefs);
- },
-
-+ deinit() {
-+ return SessionWriterInternal.deinit();
-+ },
-+
- /**
- * Write the contents of the session file.
- *
- * @param state - May get changed on shutdown.
- */
-@@ -80,10 +86,17 @@
- return await SessionWriterInternal.wipe();
- } finally {
- unlock();
- }
- },
-+
-+ /**
-+ * *Test Only* Return the SessionWriter's length hint for writing JSON.
-+ */
-+ get _jsonLengthHint() {
-+ return SessionWriterInternal.jsonLengthHint;
-+ },
- };
-
- const SessionWriterInternal = {
- // Path to the files used by the SessionWriter
- Paths: null,
-@@ -104,10 +117,19 @@
- /**
- * Number of old upgrade backups that are being kept
- */
- maxUpgradeBackups: null,
-
-+ /**
-+ * The size of the last write with IOUtils.writeJSON.
-+ *
-+ * Because SessionWriter writes such a large object graph we will otherwise
-+ * spend a large portion of `write()` doing memory allocations and memcpy
-+ * when serializing the session file to disk.
-+ */
-+ jsonLengthHint: 0,
-+
- /**
- * Initialize (or reinitialize) the writer.
- *
- * @param {string} origin Which of sessionstore.js or its backups
- * was used. One of the `STATE_*` constants defined above.
-@@ -136,13 +158,20 @@
- this.Paths = paths;
- this.maxUpgradeBackups = prefs.maxUpgradeBackups;
- this.maxSerializeBack = prefs.maxSerializeBack;
- this.maxSerializeForward = prefs.maxSerializeForward;
- this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup;
-+
-+ Services.obs.addObserver(this, BROWSER_PURGE_SESSION_HISTORY);
-+
- return { result: true };
- },
-
-+ deinit() {
-+ Services.obs.removeObserver(this, BROWSER_PURGE_SESSION_HISTORY);
-+ },
-+
- /**
- * Write the session to disk.
- * Write the session to disk, performing any necessary backup
- * along the way.
- *
-@@ -208,36 +237,42 @@
- // We are shutting down. At this stage, we know that
- // $Paths.clean is either absent or corrupted. If it was
- // originally present and valid, it has been moved to
- // $Paths.cleanBackup a long time ago. We can therefore write
- // with the guarantees that we erase no important data.
-- await IOUtils.writeJSON(this.Paths.clean, state, {
-+ const result = await IOUtils.writeJSON(this.Paths.clean, state, {
- tmpPath: this.Paths.clean + ".tmp",
- compress: true,
-+ lengthHint: this.jsonLengthHint,
- });
-+ this.jsonLengthHint = result.jsonLength;
- fileStat = await IOUtils.stat(this.Paths.clean);
- } else if (this.state == STATE_RECOVERY) {
- // At this stage, either $Paths.recovery was written >= 15
- // seconds ago during this session or we have just started
- // from $Paths.recovery left from the previous session. Either
- // way, $Paths.recovery is good. We can move $Path.backup to
- // $Path.recoveryBackup without erasing a good file with a bad
- // file.
-- await IOUtils.writeJSON(this.Paths.recovery, state, {
-+ const result = await IOUtils.writeJSON(this.Paths.recovery, state, {
- tmpPath: this.Paths.recovery + ".tmp",
- backupFile: this.Paths.recoveryBackup,
- compress: true,
-+ lengthHint: this.jsonLengthHint,
- });
-+ this.jsonLengthHint = result.jsonLength;
- fileStat = await IOUtils.stat(this.Paths.recovery);
- } else {
- // In other cases, either $Path.recovery is not necessary, or
- // it doesn't exist or it has been corrupted. Regardless,
- // don't backup $Path.recovery.
-- await IOUtils.writeJSON(this.Paths.recovery, state, {
-+ const result = await IOUtils.writeJSON(this.Paths.recovery, state, {
- tmpPath: this.Paths.recovery + ".tmp",
- compress: true,
-+ lengthHint: this.jsonLengthHint,
- });
-+ this.jsonLengthHint = result.jsonLength;
- fileStat = await IOUtils.stat(this.Paths.recovery);
- }
-
- telemetry.writeFileMs = Date.now() - startWriteMs;
- telemetry.fileSizeBytes = fileStat.size;
-@@ -420,6 +455,18 @@
-
- if (exn) {
- throw exn;
- }
- },
-+
-+ observe(_subject, topic, _data) {
-+ switch (topic) {
-+ case BROWSER_PURGE_SESSION_HISTORY:
-+ this._onPurgeSessionHistory();
-+ break;
-+ }
-+ },
-+
-+ _onPurgeSessionHistory() {
-+ this.jsonLengthHint = 0;
-+ },
- };
-diff --git a/browser/components/sessionstore/test/unit/test_write_json_length_hint.js b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
-new file mode 100644
---- /dev/null
-+++ b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
-@@ -0,0 +1,91 @@
-+/* Any copyright is dedicated to the Public Domain.
-+ http://creativecommons.org/publicdomain/zero/1.0/ */
-+
-+"use strict";
-+
-+const { updateAppInfo } = ChromeUtils.importESModule(
-+ "resource://testing-common/AppInfo.sys.mjs"
-+);
-+
-+const profile = do_get_profile();
-+
-+updateAppInfo({
-+ name: "SessionRestoreTest",
-+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
-+ version: "1",
-+ platformVersion: "",
-+});
-+
-+const { SessionFile } = ChromeUtils.importESModule(
-+ "resource:///modules/sessionstore/SessionFile.sys.mjs"
-+);
-+const { SessionWriter } = ChromeUtils.importESModule(
-+ "resource:///modules/sessionstore/SessionWriter.sys.mjs"
-+);
-+
-+add_setup(async function setup() {
-+ const source = do_get_file("data/sessionstore_valid.js");
-+ source.copyTo(profile, "sessionstore.js");
-+
-+ await writeCompressedFile(
-+ SessionFile.Paths.clean.replace("jsonlz4", "js"),
-+ SessionFile.Paths.clean
-+ );
-+
-+ await SessionFile.read();
-+});
-+
-+add_task(async function test_json_length_hint() {
-+ await IOUtils.writeJSON(PathUtils.join(PathUtils.profileDir, "dingus"), {
-+ gunk: true,
-+ });
-+
-+ Assert.equal(
-+ SessionWriter._jsonLengthHint,
-+ 0,
-+ "SessionWriter length hint starts at 0"
-+ );
-+
-+ await SessionFile.write({});
-+
-+ const lengthHint = SessionWriter._jsonLengthHint;
-+
-+ Assert.equal(
-+ SessionWriter._jsonLengthHint,
-+ JSON.stringify({}).length,
-+ "SessionWriter should cache length hint"
-+ );
-+
-+ const contents = await IOUtils.readJSON(
-+ PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json")
-+ );
-+ await SessionFile.write(contents);
-+
-+ Assert.notEqual(
-+ SessionWriter._jsonLengthHint,
-+ lengthHint,
-+ "SessionWriter length hint updated"
-+ );
-+
-+ Assert.greater(
-+ SessionWriter._jsonLengthHint,
-+ lengthHint,
-+ "SessionWriteLength hint is now larger"
-+ );
-+
-+ Services.obs.notifyObservers(null, "browser:purge-session-history");
-+
-+ Assert.equal(
-+ SessionWriter._jsonLengthHint,
-+ 0,
-+ "browser:purge-session-history notification cleans length hint"
-+ );
-+
-+ await SessionFile.write(contents);
-+
-+ Assert.notEqual(
-+ SessionWriter._jsonLengthHint,
-+ lengthHint,
-+ "SessionWriter length hint updated"
-+ );
-+});
-diff --git a/browser/components/sessionstore/test/unit/xpcshell.toml b/browser/components/sessionstore/test/unit/xpcshell.toml
---- a/browser/components/sessionstore/test/unit/xpcshell.toml
-+++ b/browser/components/sessionstore/test/unit/xpcshell.toml
-@@ -39,5 +39,7 @@
- skip-if = [
- "condprof", # Bug 1769154
- ]
-
- ["test_startup_session_async.js"]
-+
-+["test_write_json_length_hint.js"]
-
diff --git a/src/external-patches/firefox/session_store_use_size_hint/D298708.patch b/src/external-patches/firefox/session_store_use_size_hint/D298708.patch
new file mode 100644
index 000000000..965ab02c5
--- /dev/null
+++ b/src/external-patches/firefox/session_store_use_size_hint/D298708.patch
@@ -0,0 +1,280 @@
+diff --git a/browser/components/sessionstore/SessionWriter.sys.mjs b/browser/components/sessionstore/SessionWriter.sys.mjs
+--- a/browser/components/sessionstore/SessionWriter.sys.mjs
++++ b/browser/components/sessionstore/SessionWriter.sys.mjs
+@@ -80,10 +80,14 @@
+ return await SessionWriterInternal.wipe();
+ } finally {
+ unlock();
+ }
+ },
++
++ get _jsonLengthHint() {
++ return SessionWriterInternal._lastJsonLength;
++ },
+ };
+
+ const SessionWriterInternal = {
+ // Path to the files used by the SessionWriter
+ Paths: null,
+@@ -104,10 +108,14 @@
+ /**
+ * Number of old upgrade backups that are being kept
+ */
+ maxUpgradeBackups: null,
+
++ // Estimated JSON string length from the previous write, used to pre-size
++ // the serialization buffer and avoid incremental reallocations.
++ _lastJsonLength: 0,
++
+ /**
+ * Initialize (or reinitialize) the writer.
+ *
+ * @param {string} origin Which of sessionstore.js or its backups
+ * was used. One of the `STATE_*` constants defined above.
+@@ -201,48 +209,60 @@
+ }
+ }
+
+ let startWriteMs = Date.now();
+ let fileStat;
++ // Add 5% headroom to the hint so small growth between saves doesn't
++ // cause reallocs. The compressed-size-based estimate already has
++ // sufficient margin from the 4x multiplier.
++ let jsonLengthHint = Math.ceil(this._lastJsonLength * 1.05);
++
++ let uncompressedBytes;
+
+ if (options.isFinalWrite) {
+ // We are shutting down. At this stage, we know that
+ // $Paths.clean is either absent or corrupted. If it was
+ // originally present and valid, it has been moved to
+ // $Paths.cleanBackup a long time ago. We can therefore write
+ // with the guarantees that we erase no important data.
+- await IOUtils.writeJSON(this.Paths.clean, state, {
++ uncompressedBytes = await IOUtils.writeJSON(this.Paths.clean, state, {
+ tmpPath: this.Paths.clean + ".tmp",
+ compress: true,
++ jsonLengthHint,
+ });
+ fileStat = await IOUtils.stat(this.Paths.clean);
+ } else if (this.state == STATE_RECOVERY) {
+ // At this stage, either $Paths.recovery was written >= 15
+ // seconds ago during this session or we have just started
+ // from $Paths.recovery left from the previous session. Either
+ // way, $Paths.recovery is good. We can move $Path.backup to
+ // $Path.recoveryBackup without erasing a good file with a bad
+ // file.
+- await IOUtils.writeJSON(this.Paths.recovery, state, {
++ uncompressedBytes = await IOUtils.writeJSON(this.Paths.recovery, state, {
+ tmpPath: this.Paths.recovery + ".tmp",
+ backupFile: this.Paths.recoveryBackup,
+ compress: true,
++ jsonLengthHint,
+ });
+ fileStat = await IOUtils.stat(this.Paths.recovery);
+ } else {
+ // In other cases, either $Path.recovery is not necessary, or
+ // it doesn't exist or it has been corrupted. Regardless,
+ // don't backup $Path.recovery.
+- await IOUtils.writeJSON(this.Paths.recovery, state, {
++ uncompressedBytes = await IOUtils.writeJSON(this.Paths.recovery, state, {
+ tmpPath: this.Paths.recovery + ".tmp",
+ compress: true,
++ jsonLengthHint,
+ });
+ fileStat = await IOUtils.stat(this.Paths.recovery);
+ }
+
+ telemetry.writeFileMs = Date.now() - startWriteMs;
+ telemetry.fileSizeBytes = fileStat.size;
++ // Use the actual pre-compression size from this write as the hint
++ // for the next write's buffer allocation.
++ this._lastJsonLength = uncompressedBytes;
+ lazy.sessionStoreLogger.debug(
+ `SessionWriter.write wrote ${telemetry.fileSizeBytes} bytes in ${telemetry.writeFileMs}ms`
+ );
+ } catch (ex) {
+ // Don't throw immediately
+@@ -375,10 +395,11 @@
+ } catch (ex) {
+ exn = exn || ex;
+ }
+
+ this.state = STATE_EMPTY;
++ this._lastJsonLength = 0;
+ if (exn) {
+ throw exn;
+ }
+
+ return { result: true };
+diff --git a/browser/components/sessionstore/test/unit/test_write_json_length_hint.js b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js
+@@ -0,0 +1,73 @@
++/* Any copyright is dedicated to the Public Domain.
++ http://creativecommons.org/publicdomain/zero/1.0/ */
++
++"use strict";
++
++const { SessionWriter } = ChromeUtils.importESModule(
++ "resource:///modules/sessionstore/SessionWriter.sys.mjs"
++);
++
++const profd = do_get_profile();
++const { SessionFile } = ChromeUtils.importESModule(
++ "resource:///modules/sessionstore/SessionFile.sys.mjs"
++);
++
++const { updateAppInfo } = ChromeUtils.importESModule(
++ "resource://testing-common/AppInfo.sys.mjs"
++);
++updateAppInfo({
++ name: "SessionRestoreTest",
++ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
++ version: "1",
++ platformVersion: "",
++});
++
++add_setup(async function () {
++ let source = do_get_file("data/sessionstore_valid.js");
++ source.copyTo(profd, "sessionstore.js");
++ await writeCompressedFile(
++ SessionFile.Paths.clean.replace("jsonlz4", "js"),
++ SessionFile.Paths.clean
++ );
++ await SessionFile.read();
++});
++
++add_task(async function test_length_hint_updates_after_write() {
++ Assert.equal(
++ SessionWriter._jsonLengthHint,
++ 0,
++ "Length hint starts at 0"
++ );
++
++ await SessionFile.write({});
++
++ let hintAfterSmall = SessionWriter._jsonLengthHint;
++ Assert.equal(
++ hintAfterSmall,
++ JSON.stringify({}).length,
++ "Hint matches the uncompressed JSON byte length"
++ );
++
++ let largerState = await IOUtils.readJSON(
++ PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json")
++ );
++ await SessionFile.write(largerState);
++
++ Assert.greater(
++ SessionWriter._jsonLengthHint,
++ hintAfterSmall,
++ "Hint grows after writing a larger state"
++ );
++});
++
++add_task(async function test_length_hint_resets_on_wipe() {
++ await SessionFile.write({ windows: [{ tabs: [{ entries: [] }] }] });
++ Assert.greater(SessionWriter._jsonLengthHint, 0, "Hint is nonzero");
++
++ await SessionFile.wipe();
++ Assert.equal(
++ SessionWriter._jsonLengthHint,
++ 0,
++ "Hint resets to 0 after wipe"
++ );
++});
+diff --git a/browser/components/sessionstore/test/unit/xpcshell.toml b/browser/components/sessionstore/test/unit/xpcshell.toml
+--- a/browser/components/sessionstore/test/unit/xpcshell.toml
++++ b/browser/components/sessionstore/test/unit/xpcshell.toml
+@@ -39,5 +39,10 @@
+ skip-if = [
+ "condprof", # Bug 1769154
+ ]
+
+ ["test_startup_session_async.js"]
++
++["test_write_json_length_hint.js"]
++support-files = [
++ "data/sessionstore_complete.json",
++]
+diff --git a/dom/chrome-webidl/IOUtils.webidl b/dom/chrome-webidl/IOUtils.webidl
+--- a/dom/chrome-webidl/IOUtils.webidl
++++ b/dom/chrome-webidl/IOUtils.webidl
+@@ -101,12 +101,12 @@
+ *
+ * @param path An absolute file path
+ * @param value The value to be serialized.
+ * @param options Options for writing the file. The "append" mode is not supported.
+ *
+- * @return Resolves with the number of bytes successfully written to the file,
+- * otherwise rejects with a DOMException.
++ * @return Resolves with the pre-compression size of the serialized JSON in
++ * bytes (UTF-8), otherwise rejects with a DOMException.
+ */
+ [NewObject]
+ Promise writeJSON(DOMString path, any value, optional WriteOptions options = {});
+ /**
+ * Moves the file from |sourcePath| to |destPath|, creating necessary parents.
+@@ -564,10 +564,16 @@
+ boolean flush = false;
+ /**
+ * If true, compress the data with LZ4-encoding before writing to the file.
+ */
+ boolean compress = false;
++ /**
++ * For |writeJSON|, a hint for the expected JSON string length in UTF-16 code
++ * units. When provided, the JSON serializer pre-allocates a buffer of this
++ * size to avoid incremental reallocations.
++ */
++ unsigned long long jsonLengthHint = 0;
+ };
+
+ /**
+ * Options to be passed to the |IOUtils.move| method.
+ */
+diff --git a/xpcom/ioutils/IOUtils.cpp b/xpcom/ioutils/IOUtils.cpp
+--- a/xpcom/ioutils/IOUtils.cpp
++++ b/xpcom/ioutils/IOUtils.cpp
+@@ -622,13 +622,22 @@
+ return;
+ }
+
+ JSContext* cx = aGlobal.Context();
+ JS::Rooted rootedValue(cx, aValue);
++ size_t lengthHint = aOptions.mJsonLengthHint;
+ nsString string;
+- if (!nsContentUtils::StringifyJSON(cx, aValue, string,
+- UndefinedIsNullStringLiteral)) {
++ if (lengthHint) {
++ string.SetCapacity(lengthHint);
++ }
++ if (!JS_StringifyWithLengthHint(
++ cx, &rootedValue, nullptr, JS::NullHandleValue,
++ [](const char16_t* aBuf, uint32_t aLen, void* aData) -> bool {
++ return static_cast(aData)->Append(aBuf, aLen,
++ fallible);
++ },
++ &string, lengthHint)) {
+ JS::Rooted exn(cx, JS::UndefinedValue());
+ if (JS_GetPendingException(cx, &exn)) {
+ JS_ClearPendingException(cx);
+ promise->MaybeReject(exn);
+ } else {
+@@ -648,11 +657,13 @@
+ return Err(IOError(
+ NS_ERROR_OUT_OF_MEMORY,
+ "Failed to write to `%s': could not allocate buffer",
+ file->HumanReadablePath().get()));
+ }
+- return WriteSync(file, AsBytes(Span(utf8Str)), opts);
++ uint32_t uncompressedSize = utf8Str.Length();
++ MOZ_TRY(WriteSync(file, AsBytes(Span(utf8Str)), opts));
++ return uncompressedSize;
+ });
+ });
+ }
+
+ /* static */
+
diff --git a/src/external-patches/manifest.json b/src/external-patches/manifest.json
index ea0199ba6..37edc43f6 100644
--- a/src/external-patches/manifest.json
+++ b/src/external-patches/manifest.json
@@ -10,7 +10,19 @@
// Specifically trying to target FeatureCallout.sys.mjs's change.
// IMPORTANT: Make sure Feature callouts STILL use native popopvers when
// syncing from upstream, as this is a critical part of the patch.
- "+ nonnativepopover=\"true\"": "+ "
+ "+ nonnativepopover=\"true\"": "+ ",
+
+ // Fix conflicts with upstream changes.
+ "--menuitem-border-radius: var(--panel-menuitem-border-radius)": "--menuitem-border-radius: var(--arrowpanel-menuitem-border-radius)",
+ "--menuitem-padding: var(--panel-menuitem-padding)": "--menuitem-padding: var(--arrowpanel-menuitem-padding)",
+ "--menuitem-margin: var(--panel-menuitem-margin)": "--menuitem-margin: var(--arrowpanel-menuitem-margin)",
+
+ " \n #include \"nsCocoaWindow.h\"\n \n #include \"nsISupportsPrimitives.h\"\n #include \"nsArrayUtils.h\"":
+ " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n \n #include \"nsCocoaWindow.h\"\n \n #include \"nsISupportsPrimitives.h\"\n #include \"nsArrayUtils.h\"",
+ " #include \"nsISupportsPrimitives.h\"\n": "",
+
+ " Atom(\"nonnative\", \"nonnative\"),\n": "",
+ "Atom(\"noscript\", \"noscript\"),": "Atom(\"noscript\", \"noscript\"),\n Atom(\"noshade\", \"noshade\"),"
}
},
{
@@ -21,6 +33,12 @@
"\n\n": "\n // may want to figure out a more robust way to detect abandonment."
}
},
+ {
+ "type": "local",
+ // TODO: Convert into https://phabricator.services.mozilla.com/D298079
+ // once it gets accepted.
+ "path": "firefox/allow_backdrop_to_work_on_transparency.patch"
+ },
{
"type": "local",
"path": "firefox/no_liquid_glass_icon.patch"
@@ -44,8 +62,7 @@
"type": "phabricator",
"ids": [
"D247145",
- "D247215",
- "D247217"
+ "D298708"
],
"name": "Session store use size hint"
},
diff --git a/src/toolkit/content/widgets/panel-js.patch b/src/toolkit/content/widgets/panel-js.patch
index 011e87349..276e72727 100644
--- a/src/toolkit/content/widgets/panel-js.patch
+++ b/src/toolkit/content/widgets/panel-js.patch
@@ -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");
diff --git a/src/toolkit/modules/JSONFile-sys-mjs.patch b/src/toolkit/modules/JSONFile-sys-mjs.patch
index ca854e039..f289c4b40 100644
--- a/src/toolkit/modules/JSONFile-sys-mjs.patch
+++ b/src/toolkit/modules/JSONFile-sys-mjs.patch
@@ -1,5 +1,5 @@
diff --git a/toolkit/modules/JSONFile.sys.mjs b/toolkit/modules/JSONFile.sys.mjs
-index 397991e4af8f49b6365d729fc11267b5c1113400..1955b7ff1d428e891f5ef066e7a4ac25aa5ec9b4 100644
+index 397991e4af8f49b6365d729fc11267b5c1113400..9b1d6fd3850b239000a3c4d2a2d5799a0989f4e3 100644
--- a/toolkit/modules/JSONFile.sys.mjs
+++ b/toolkit/modules/JSONFile.sys.mjs
@@ -132,6 +132,7 @@ export function JSONFile(config) {
@@ -16,14 +16,14 @@ index 397991e4af8f49b6365d729fc11267b5c1113400..1955b7ff1d428e891f5ef066e7a4ac25
try {
- await IOUtils.writeJSON(
+ if (this._useSizeHints && this._lastSavedSize) {
-+ this._options.lengthHint = this._lastSavedSize;
++ this._options.jsonLengthHint = Math.ceil(this._lastSavedSize * 1.05);
+ }
+ const result = await IOUtils.writeJSON(
this.path,
this._data,
Object.assign({ tmpPath: this.path + ".tmp" }, this._options)
);
-+ this._lastSavedSize = this._useSizeHints ? result.jsonLength : null;
++ this._lastSavedSize = this._useSizeHints ? result : null;
} catch (ex) {
if (typeof this._data.toJSONSafe == "function") {
// If serialization fails, try fallback safe JSON converter.
diff --git a/src/widget/SwipeTracker-cpp.patch b/src/widget/SwipeTracker-cpp.patch
index 05969b0fb..54dd51238 100644
--- a/src/widget/SwipeTracker-cpp.patch
+++ b/src/widget/SwipeTracker-cpp.patch
@@ -1,5 +1,5 @@
diff --git a/widget/SwipeTracker.cpp b/widget/SwipeTracker.cpp
-index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..e2bf27c0130701f1d50990b60a5ef76e93c5a6bf 100644
+index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..7225d4264c9ec71ef1e5e717a1a62c4ef0aff0b7 100644
--- a/widget/SwipeTracker.cpp
+++ b/widget/SwipeTracker.cpp
@@ -3,6 +3,7 @@
@@ -10,7 +10,17 @@ index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..e2bf27c0130701f1d50990b60a5ef76e
#include "InputData.h"
#include "mozilla/FlushType.h"
-@@ -90,7 +91,7 @@ bool SwipeTracker::ComputeSwipeSuccess() const {
+@@ -67,6 +68,9 @@ double SwipeTracker::SwipeSuccessTargetValue() const {
+ }
+
+ double SwipeTracker::ClampToAllowedRange(double aGestureAmount) const {
++ if (StaticPrefs::zen_swipe_is_fast_swipe()) {
++ return aGestureAmount;
++ }
+ // gestureAmount needs to stay between -1 and 0 when swiping right and
+ // between 0 and 1 when swiping left.
+ double min =
+@@ -90,7 +94,7 @@ bool SwipeTracker::ComputeSwipeSuccess() const {
return (mGestureAmount * targetValue +
mCurrentVelocity * targetValue *
@@ -19,3 +29,13 @@ index 887d06d3bd9cdaa934880e0ae7a11ec8b737fb61..e2bf27c0130701f1d50990b60a5ef76e
kSwipeSuccessThreshold;
}
+@@ -141,7 +145,8 @@ nsEventStatus SwipeTracker::ProcessEvent(
+ // display the UI as if we were at the success threshold as that would
+ // give a false indication that navigation would happen.
+ if (!computedSwipeSuccess && (eventAmount >= kSwipeSuccessThreshold ||
+- eventAmount <= -kSwipeSuccessThreshold)) {
++ eventAmount <= -kSwipeSuccessThreshold)
++ && !StaticPrefs::zen_swipe_is_fast_swipe()) {
+ eventAmount = 0.999 * kSwipeSuccessThreshold;
+ if (mGestureAmount < 0.f) {
+ eventAmount = -eventAmount;
diff --git a/src/zen/boosts/ZenBoostStyles.sys.mjs b/src/zen/boosts/ZenBoostStyles.sys.mjs
index cc6ca22e5..40c230f6f 100644
--- a/src/zen/boosts/ZenBoostStyles.sys.mjs
+++ b/src/zen/boosts/ZenBoostStyles.sys.mjs
@@ -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`;
}
diff --git a/src/zen/boosts/actors/ZenBoostsChild.sys.mjs b/src/zen/boosts/actors/ZenBoostsChild.sys.mjs
index 2d5aaba28..4217c3053 100644
--- a/src/zen/boosts/actors/ZenBoostsChild.sys.mjs
+++ b/src/zen/boosts/actors/ZenBoostsChild.sys.mjs
@@ -76,7 +76,7 @@ export class ZenBoostsChild extends JSWindowActorChild {
* > #define NS_RGBA(_r, _g, _b, _a) \
* > ((nscolor)(((_a) << 24) | ((_b) << 16) | ((_g) << 8) | (_r)))
*
- * Converts [r, g, b] array to NSColor
+ * Converts [r, g, b] array to (uint32_t) NSColor
* Make a color out of r,g,b,a values. This assumes that the r,g,b,a
* values are properly constrained to 0-255.
*
@@ -92,7 +92,8 @@ export class ZenBoostsChild extends JSWindowActorChild {
// be fully opaque and we need an extra byte to store the contrast value. This allows
// us to still use an nscolor as parameter instead of having to deal with WebIDL structs
// shenanigans.
- return (contrast << 24) | (b << 16) | (g << 8) | r;
+ // Take into account that its an unsigned int
+ return ((contrast << 24) | (b << 16) | (g << 8) | r) >>> 0;
}
/**
@@ -173,7 +174,7 @@ export class ZenBoostsChild extends JSWindowActorChild {
}
this.#removeEventListeners();
break;
- case "DOMDocElementInserted":
+ case "DOMWindowCreated":
this.#applyBoostForPageIfAvailable();
break;
default:
diff --git a/src/zen/boosts/nsZenBCOverrides.cpp b/src/zen/boosts/nsZenBCOverrides.cpp
index 52536ddac..89c75b04e 100644
--- a/src/zen/boosts/nsZenBCOverrides.cpp
+++ b/src/zen/boosts/nsZenBCOverrides.cpp
@@ -38,6 +38,18 @@ void BrowsingContext::WalkPresContexts(Callback&& aCallback) {
});
}
+static void RefreshBoostCacheIfMatchesCurrent(BrowsingContext* aChanged) {
+ auto* backend = zen::nsZenBoostsBackend::GetInstance();
+ if (!backend) {
+ return;
+ }
+ auto current = backend->GetCurrentBrowsingContext();
+ if (!current || current->Top() != aChanged) {
+ return;
+ }
+ backend->RefreshCachedBoostState();
+}
+
/**
* @brief Called when the ZenBoostsData field is set on a browsing context.
* Triggers a restyle if the boost data has changed.
@@ -49,6 +61,7 @@ void BrowsingContext::DidSet(FieldIndex,
if (ZenBoostsData() == aOldValue) {
return;
}
+ RefreshBoostCacheIfMatchesCurrent(this);
PresContextAffectingFieldChanged();
TRIGGER_PRES_CONTEXT_RESTYLE();
}
@@ -64,6 +77,7 @@ void BrowsingContext::DidSet(FieldIndex,
if (IsZenBoostsInverted() == aOldValue) {
return;
}
+ RefreshBoostCacheIfMatchesCurrent(this);
PresContextAffectingFieldChanged();
TRIGGER_PRES_CONTEXT_RESTYLE();
}
diff --git a/src/zen/boosts/nsZenBoostsBackend.cpp b/src/zen/boosts/nsZenBoostsBackend.cpp
index 825cc971c..9c545ddf4 100644
--- a/src/zen/boosts/nsZenBoostsBackend.cpp
+++ b/src/zen/boosts/nsZenBoostsBackend.cpp
@@ -145,7 +145,7 @@ inline static auto zenPrecomputeAccent(nscolor aAccentColor) {
return aOriginalColor;
}
- const float inv255 = 1.0f / 255.0f;
+ constexpr float inv255 = 1.0f / 255.0f;
const float blendFactor = contrast * inv255;
// sRGB -> linear
@@ -261,7 +261,9 @@ inline static nscolor zenInvertColorChannel(nscolor aColor) {
}
/**
- * @brief Retrieves the current boost data from the browsing context.
+ * @brief Retrieves the current boost data from the browsing context. When
+ * called without aPresContext, reads the precomputed cache populated on
+ * presshell entry; otherwise resolves from the supplied PresContext.
*/
ZEN_HOT_FUNCTION
inline static void GetZenBoostsDataFromBrowsingContext(
@@ -272,11 +274,14 @@ inline static void GetZenBoostsDataFromBrowsingContext(
!SHOULD_APPLY_BOOSTS_TO_ANONYMOUS_CONTENT())) {
return;
}
- auto browsingContext = zenBoosts->GetCurrentBrowsingContext();
- if (aPresContext) {
- if (auto document = aPresContext->Document()) {
- browsingContext = document->GetBrowsingContext();
- }
+ if (!aPresContext) {
+ *aData = zenBoosts->mCachedCurrentAccent;
+ *aIsInverted = zenBoosts->mCachedCurrentInverted;
+ return;
+ }
+ mozilla::dom::BrowsingContext* browsingContext = nullptr;
+ if (auto document = aPresContext->Document()) {
+ browsingContext = document->GetBrowsingContext();
}
if (!browsingContext) {
return;
@@ -318,6 +323,18 @@ auto nsZenBoostsBackend::onPresShellEntered(mozilla::dom::Document* aDocument)
return;
}
mCurrentBrowsingContext = browsingContext;
+ RefreshCachedBoostState();
+}
+
+auto nsZenBoostsBackend::RefreshCachedBoostState() -> void {
+ if (!mCurrentBrowsingContext) {
+ mCachedCurrentAccent = 0;
+ mCachedCurrentInverted = false;
+ return;
+ }
+ auto top = mCurrentBrowsingContext->Top();
+ mCachedCurrentAccent = top->ZenBoostsData();
+ mCachedCurrentInverted = top->IsZenBoostsInverted();
}
[[nodiscard]] ZEN_HOT_FUNCTION auto
diff --git a/src/zen/boosts/nsZenBoostsBackend.h b/src/zen/boosts/nsZenBoostsBackend.h
index 943a5ac8a..b79d7ef4e 100644
--- a/src/zen/boosts/nsZenBoostsBackend.h
+++ b/src/zen/boosts/nsZenBoostsBackend.h
@@ -64,11 +64,27 @@ class nsZenBoostsBackend final {
*/
auto onPresShellEntered(mozilla::dom::Document* aDocument) -> void;
+ /**
+ * @brief Refresh the cached boost state from the current top BrowsingContext.
+ * Called from onPresShellEntered and from BrowsingContext::DidSet hooks when
+ * the underlying boost fields change.
+ */
+ auto RefreshCachedBoostState() -> void;
+
[[nodiscard]]
inline auto GetCurrentBrowsingContext() const {
return mCurrentBrowsingContext;
}
+ /**
+ * Cached boost data for the current top BrowsingContext, refreshed on
+ * presshell entry and on DidSet hooks. Read by the per-color hot path so
+ * that boost-off pages don't pay for a BrowsingContext walk on every color
+ * resolve.
+ */
+ ZenBoostData mCachedCurrentAccent = 0;
+ bool mCachedCurrentInverted = false;
+
private:
/**
* The presshell of the current document being rendered.
diff --git a/src/zen/boosts/zen-boost-editor.inc.xhtml b/src/zen/boosts/zen-boost-editor.inc.xhtml
index 9694be8ef..c3a857092 100644
--- a/src/zen/boosts/zen-boost-editor.inc.xhtml
+++ b/src/zen/boosts/zen-boost-editor.inc.xhtml
@@ -115,7 +115,7 @@
-
+
diff --git a/src/zen/common/modules/ZenMenubar.mjs b/src/zen/common/modules/ZenMenubar.mjs
index 9616d0228..08ce51e81 100644
--- a/src/zen/common/modules/ZenMenubar.mjs
+++ b/src/zen/common/modules/ZenMenubar.mjs
@@ -92,7 +92,7 @@ export class nsZenMenuBar {
key="zen-workspace-forward"/>
`);
diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs
index 45d9fd40e..f5d42d983 100644
--- a/src/zen/common/modules/ZenUIManager.mjs
+++ b/src/zen/common/modules/ZenUIManager.mjs
@@ -705,7 +705,16 @@ window.gZenUIManager = {
this.urlbarShowDomainOnly
) {
let url = BrowserUIUtils.removeSingleTrailingSlashFromURL(aURL);
- return url.startsWith("https://") ? url.split("/")[2] : url;
+ requestIdleCallback(() => {
+ // Scroll the urlbar all the way to the right so that
+ // the domain is always visible when the url is too long.
+ gURLBar.inputField.scrollLeft = gURLBar.inputField.scrollWidth;
+ });
+ let stripped = url.startsWith("https://") ? url.split("/")[2] : url;
+ if (stripped.startsWith("www.")) {
+ stripped = stripped.substring(4);
+ }
+ return stripped;
}
return BrowserUIUtils.trimURL(aURL);
},
@@ -1376,7 +1385,7 @@ window.gZenVerticalTabsManager = {
buttonsTarget.append(this._topButtonsSeparatorElement);
}
for (const button of elements) {
- this._topButtonsSeparatorElement.after(button);
+ this.appendCustomizableItem(this._topButtonsSeparatorElement, button);
}
buttonsTarget.prepend(
document.getElementById("unified-extensions-button")
@@ -1579,16 +1588,36 @@ window.gZenVerticalTabsManager = {
Services.prefs.setBoolPref("zen.tabs.vertical.right-side", newVal);
},
- appendCustomizableItem(target, child, placements) {
+ appendCustomizableItem(target, child, placements = []) {
if (
- target.id === "zen-sidebar-top-buttons-customization-target" &&
this._hasSetSingleToolbar &&
- placements.includes(child.id)
+ (target.id === "zen-sidebar-top-buttons-customization-target" ||
+ target === this._topButtonsSeparatorElement)
) {
- this._topButtonsSeparatorElement.before(child);
- return;
+ if (placements.includes(child.id)) {
+ this._topButtonsSeparatorElement.before(child);
+ return;
+ } else if (
+ child.hasAttribute("data-extensionid") &&
+ Services.prefs.getBoolPref("zen.view.overflow-webext-toolbar", true)
+ ) {
+ if (gURLBar._isOverflowingItems) {
+ const overflowElements = document.getElementById(
+ "zen-overflow-extensions-list"
+ );
+ overflowElements.appendChild(child);
+ } else {
+ const element = document.getElementById("page-action-buttons");
+ element.before(child);
+ }
+ return;
+ }
+ }
+ if (target === this._topButtonsSeparatorElement) {
+ this._topButtonsSeparatorElement.after(child);
+ } else {
+ target.appendChild(child);
}
- target.appendChild(child);
},
async renameTabKeydown(event) {
diff --git a/src/zen/common/styles/schemes/dark.inc.css b/src/zen/common/styles/schemes/dark.inc.css
index 9b20f1a6a..e66510082 100644
--- a/src/zen/common/styles/schemes/dark.inc.css
+++ b/src/zen/common/styles/schemes/dark.inc.css
@@ -4,3 +4,4 @@
color-scheme: dark;
--zen-urlbar-outline-offset: -2px;
+--zen-urlbar-filter: blur(40px) saturate(110%) brightness(25%) contrast(100%);
diff --git a/src/zen/common/styles/schemes/light.inc.css b/src/zen/common/styles/schemes/light.inc.css
index d010f2f19..81daec9b5 100644
--- a/src/zen/common/styles/schemes/light.inc.css
+++ b/src/zen/common/styles/schemes/light.inc.css
@@ -4,3 +4,4 @@
color-scheme: light;
--zen-urlbar-outline-offset: 0px;
+--zen-urlbar-filter: blur(40px) saturate(110%) brightness(75%) contrast(100%);
diff --git a/src/zen/common/styles/zen-animations.css b/src/zen/common/styles/zen-animations.css
index 8e5aaee3e..7784ef08f 100644
--- a/src/zen/common/styles/zen-animations.css
+++ b/src/zen/common/styles/zen-animations.css
@@ -37,6 +37,18 @@
}
@keyframes zen-dialog-fade-in {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes zen-dialog-fade-in-shifted {
from {
opacity: 0;
transform: translateY(calc(-10% - 10px));
diff --git a/src/zen/common/styles/zen-browser-container.css b/src/zen/common/styles/zen-browser-container.css
index fb820d695..395a97c2b 100644
--- a/src/zen/common/styles/zen-browser-container.css
+++ b/src/zen/common/styles/zen-browser-container.css
@@ -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")) and -moz-pref("zen.view.hide-window-controls") {
.browserSidebarContainer:is(.deck-selected, [zen-split="true"]) .browserContainer {
transition: margin var(--zen-hidden-toolbar-transition);
@@ -59,6 +59,13 @@
--margin-top-fix: calc(-1 * var(--zen-toolbar-height-with-bookmarks) + var(--zen-element-separation));
}
+ /* stylelint-disable-next-line media-query-no-invalid */
+ @media -moz-pref("zen.view.experimental-no-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);
}
}
diff --git a/src/zen/common/styles/zen-omnibox.css b/src/zen/common/styles/zen-omnibox.css
index f943090fc..373598eb2 100644
--- a/src/zen/common/styles/zen-omnibox.css
+++ b/src/zen/common/styles/zen-omnibox.css
@@ -139,10 +139,12 @@
}
.identity-box-button,
- .urlbar-page-action {
+ .urlbar-page-action,
+ .unified-extensions-item {
opacity: 0;
height: 100%; /* To still be able to open popups */
visibility: collapse;
+ margin: 0 !important;
:root:not([supress-primary-adjustment="true"]) & {
transition:
@@ -189,6 +191,10 @@
}
}
+:root[zen-single-toolbar="true"] #urlbar[breakout-extend="true"] .unified-extensions-item {
+ display: none !important;
+}
+
/* stylelint-disable-next-line media-query-no-invalid */
@media -moz-pref("zen.urlbar.single-toolbar-show-copy-url", false) {
:root[zen-single-toolbar="true"] #zen-copy-url-button {
@@ -262,9 +268,9 @@
& .urlbar-background {
--zen-urlbar-background-base: light-dark(#fbfbfb, color-mix(in srgb, hsl(0, 0%, 6.7%), var(--zen-colors-primary) 30%));
/* stylelint-disable-next-line media-query-no-invalid */
- @media -moz-pref("zen.theme.acrylic-elements") {
+ @media -moz-pref("zen.theme.acrylic-elements") and (not (prefers-reduced-transparency: reduce)) {
--zen-urlbar-background-transparent: light-dark(
- color-mix(in srgb, var(--zen-urlbar-background-base) 90%, transparent),
+ color-mix(in srgb, white 80%, transparent),
color-mix(in srgb, var(--zen-urlbar-background-base) 65%, transparent)
);
}
@@ -276,7 +282,7 @@
/* stylelint-disable-next-line media-query-no-invalid */
@media -moz-pref("zen.theme.acrylic-elements") {
- backdrop-filter: blur(42px) saturate(110%) brightness(0.25) contrast(100%) !important;
+ backdrop-filter: var(--zen-urlbar-filter) !important;
}
}
diff --git a/src/zen/common/styles/zen-overflowing-addons.css b/src/zen/common/styles/zen-overflowing-addons.css
index cd68ea2f1..34391ce96 100644
--- a/src/zen/common/styles/zen-overflowing-addons.css
+++ b/src/zen/common/styles/zen-overflowing-addons.css
@@ -6,10 +6,12 @@
#zen-overflow-extensions-list:not(:empty) {
--uei-icon-size: 14px;
- display: flex;
+ display: grid;
gap: 8px;
padding: 8px 2px;
padding-bottom: 0;
+ border-radius: calc(var(--border-radius-medium) - 4px);
+ grid-template-columns: repeat(auto-fit, minmax(32px, 1fr));
& .unified-extensions-item {
flex: 1;
@@ -26,6 +28,7 @@
}
& .unified-extensions-item-action-button {
+ appearance: none;
background-color: var(--zen-toolbar-element-bg);
height: 30px;
margin: 0;
diff --git a/src/zen/common/styles/zen-panels/dialog.css b/src/zen/common/styles/zen-panels/dialog.css
index dbec0aae1..b5979bb4c 100644
--- a/src/zen/common/styles/zen-panels/dialog.css
+++ b/src/zen/common/styles/zen-panels/dialog.css
@@ -10,8 +10,14 @@
box-shadow: 0 10px 8px rgba(0, 0 , 0, 0.15) !important;
outline-offset: -1.5px;
- &:not([sizeto="available"]) {
+ @media not (prefers-reduced-motion: reduce) {
animation: zen-dialog-fade-in 0.3s ease-out;
+ }
+
+ .content-prompt-dialog & {
+ @media not (prefers-reduced-motion: reduce) {
+ animation: zen-dialog-fade-in-shifted 0.3s ease-out;
+ }
transform: translateY(-10%);
}
}
diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css
index 61c087bd2..2544b8b11 100644
--- a/src/zen/common/styles/zen-single-components.css
+++ b/src/zen/common/styles/zen-single-components.css
@@ -739,17 +739,13 @@
--zen-loading-progress-bar-color: color-mix(in srgb, var(--zen-primary-color), light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.5)) 70%);
position: fixed;
- top: calc(-2px - env(hairline));
+ top: calc(var(--zen-element-separation) / -2);
/* Minimum -2px, but if its larger, elemenet separation / -2 will be used as top offset, to avoid overlapping with the notification stack */
- :root:is([zen-no-padding="true"], [inDOMFullscreen="true"]) & {
+ :root:is([zen-no-padding="true"], [inDOMFullscreen="true"], :not([zen-single-toolbar="true"])) & {
top: 4px;
}
- :root[zen-single-toolbar="true"] & {
- top: calc(var(--zen-element-separation) / -2);
- }
-
left: 50%;
transform: translate(-50%, -50%) scale(0);
background: var(--zen-loading-progress-bar-color);
diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css
index 05ef5b3dc..769fb5b85 100644
--- a/src/zen/common/styles/zen-theme.css
+++ b/src/zen/common/styles/zen-theme.css
@@ -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);
diff --git a/src/zen/common/sys/ZenActorsManager.sys.mjs b/src/zen/common/sys/ZenActorsManager.sys.mjs
index 8123ad26b..766193ad3 100644
--- a/src/zen/common/sys/ZenActorsManager.sys.mjs
+++ b/src/zen/common/sys/ZenActorsManager.sys.mjs
@@ -67,7 +67,7 @@ if (!Services.appinfo.inSafeMode) {
child: {
esModuleURI: "resource:///actors/ZenBoostsChild.sys.mjs",
events: {
- DOMDocElementInserted: { capture: true },
+ DOMWindowCreated: {},
unload: {},
},
},
diff --git a/src/zen/drag-and-drop/ZenDragAndDrop.js b/src/zen/drag-and-drop/ZenDragAndDrop.js
index f2f0d6881..360e8f75f 100644
--- a/src/zen/drag-and-drop/ZenDragAndDrop.js
+++ b/src/zen/drag-and-drop/ZenDragAndDrop.js
@@ -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);
}
}
diff --git a/src/zen/glance/ZenGlanceManager.mjs b/src/zen/glance/ZenGlanceManager.mjs
index 9c738cd01..e2d83eb74 100644
--- a/src/zen/glance/ZenGlanceManager.mjs
+++ b/src/zen/glance/ZenGlanceManager.mjs
@@ -313,16 +313,18 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
// content process since it does not take into account scroll. This way, we can
// be sure that the coordinates are correct.
const tabPanelsRect = gBrowser.tabpanels.getBoundingClientRect();
+ const zoomLevel =
+ this.#currentParentTab?.linkedBrowser.browsingContext.fullZoom || 1;
const rect = new DOMRect(
- data.clientX + tabPanelsRect.left,
- data.clientY + tabPanelsRect.top,
- data.width,
- data.height
+ data.clientX / zoomLevel + tabPanelsRect.left,
+ data.clientY / zoomLevel + tabPanelsRect.top,
+ data.width / zoomLevel,
+ data.height / zoomLevel
);
return await this.#imageBitmapToObjectURL(
await window.browsingContext.currentWindowGlobal.drawSnapshot(
rect,
- 1,
+ zoomLevel,
"transparent",
undefined
)
diff --git a/src/zen/glance/actors/ZenGlanceChild.sys.mjs b/src/zen/glance/actors/ZenGlanceChild.sys.mjs
index c5da67cd2..10d9fff14 100644
--- a/src/zen/glance/actors/ZenGlanceChild.sys.mjs
+++ b/src/zen/glance/actors/ZenGlanceChild.sys.mjs
@@ -66,11 +66,13 @@ export class ZenGlanceChild extends JSWindowActorChild {
) {
rect = originalTargetRect;
}
+ // Change the rect to make sure we take into account zoom.
+ const zoom = this.browsingContext.fullZoom;
this.sendAsyncMessage("ZenGlance:RecordLinkClickData", {
- clientX: rect.left,
- clientY: rect.top,
- width: rect.width,
- height: rect.height,
+ clientX: rect.left * zoom,
+ clientY: rect.top * zoom,
+ width: rect.width * zoom,
+ height: rect.height * zoom,
});
}
diff --git a/src/zen/glance/zen-glance.css b/src/zen/glance/zen-glance.css
index b3e6d1190..0c03d05a6 100644
--- a/src/zen/glance/zen-glance.css
+++ b/src/zen/glance/zen-glance.css
@@ -83,6 +83,7 @@
& label {
max-width: 4rem;
margin-left: 8px;
+ color: white;
}
& image {
diff --git a/src/zen/kbs/ZenKeyboardShortcuts.sys.mjs b/src/zen/kbs/ZenKeyboardShortcuts.sys.mjs
index 73ae647a8..7e2254dfd 100644
--- a/src/zen/kbs/ZenKeyboardShortcuts.sys.mjs
+++ b/src/zen/kbs/ZenKeyboardShortcuts.sys.mjs
@@ -820,7 +820,7 @@ class nsZenKeyboardShortcutsLoader {
}
class nsZenKeyboardShortcutsVersioner {
- static LATEST_KBS_VERSION = 19;
+ static LATEST_KBS_VERSION = 20;
constructor() {}
@@ -1131,6 +1131,21 @@ class nsZenKeyboardShortcutsVersioner {
);
}
if (version < 18) {
+ // Migrate from version 17 to 18.
+ // Add shortcut to Create New Workspace (unbound by default)
+ data.push(
+ new KeyShortcut(
+ "zen-workspace-create",
+ "",
+ "",
+ ZEN_WORKSPACE_SHORTCUTS_GROUP,
+ nsKeyShortcutModifiers.fromObject({}),
+ "cmd_zenOpenWorkspaceCreation",
+ "zen-workspace-shortcut-create"
+ )
+ );
+ }
+ if (version < 19) {
data.push(
new KeyShortcut(
"zen-new-little-window",
@@ -1147,12 +1162,28 @@ class nsZenKeyboardShortcutsVersioner {
)
);
}
- if (version < 19) {
+ if (version < 20) {
+ let hasWorkspaceCreate = false;
for (let shortcut of data) {
if (shortcut.getID() == "zen-new-little-window") {
shortcut._setZenGlobal(true);
- break;
}
+ if (shortcut.getID() == "zen-workspace-create") {
+ hasWorkspaceCreate = true;
+ }
+ }
+ if (!hasWorkspaceCreate) {
+ data.push(
+ new KeyShortcut(
+ "zen-workspace-create",
+ "",
+ "",
+ ZEN_WORKSPACE_SHORTCUTS_GROUP,
+ nsKeyShortcutModifiers.fromObject({}),
+ "cmd_zenOpenWorkspaceCreation",
+ "zen-workspace-shortcut-create"
+ )
+ );
}
}
return data;
diff --git a/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs b/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs
index 398ebaa77..b30520da0 100644
--- a/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs
+++ b/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs
@@ -34,7 +34,11 @@ export class nsGithubLiveFolderProvider extends nsZenLiveFolderProvider {
if (
this.state.type === "pull-requests" &&
- typeof this.state.isJsonApi !== "boolean"
+ typeof this.state.isJsonApi !== "boolean" &&
+ !Services.prefs.getBoolPref(
+ "zen.live-folders.github.skip-new-pr-ui-check",
+ false
+ )
) {
const { text, status } = await this.fetch(this.state.url, {
headers: {
diff --git a/src/zen/live-folders/providers/RssLiveFolder.sys.mjs b/src/zen/live-folders/providers/RssLiveFolder.sys.mjs
index eff6bed3c..ab99fa4ad 100644
--- a/src/zen/live-folders/providers/RssLiveFolder.sys.mjs
+++ b/src/zen/live-folders/providers/RssLiveFolder.sys.mjs
@@ -62,6 +62,14 @@ export class nsRssLiveFolderProvider extends nsZenLiveFolderProvider {
if (!item.url || !item.date) {
return false;
}
+ try {
+ const parsed = Services.io.newURI(item.url);
+ if (parsed.scheme !== "http" && parsed.scheme !== "https") {
+ return false;
+ }
+ } catch {
+ return false;
+ }
if (!this.state.timeRange) {
return true;
}
@@ -73,10 +81,9 @@ export class nsRssLiveFolderProvider extends nsZenLiveFolderProvider {
for (let item of items) {
if (item.url) {
try {
- const url = new URL(item.url);
- const favicon = await lazy.PlacesUtils.favicons.getFaviconForPage(
- Services.io.newURI(url.href)
- );
+ const url = Services.io.newURI(item.url);
+ const favicon =
+ await lazy.PlacesUtils.favicons.getFaviconForPage(url);
item.icon =
favicon?.dataURI.spec ||
this.manager.window.gZenEmojiPicker.getSVGURL("logo-rss.svg");
diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs
index c3176fdb8..781fc2a3a 100644
--- a/src/zen/sessionstore/ZenWindowSync.sys.mjs
+++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs
@@ -97,11 +97,11 @@ class nsZenWindowSync {
};
/**
- * Promise that resolves when the current docshell swap operation is finished.
+ * Promise|null that resolves when the current docshell swap operation is finished.
* Used to avoid multiple simultaneous swap operations that could interfere with each other.
* For example, when focusing a window AND selecting a tab at the same time.
*/
- #docShellSwitchPromise = Promise.resolve();
+ #docShellSwitchPromise = null;
/**
* Map of sync handlers for different event types.
@@ -1360,6 +1360,9 @@ class nsZenWindowSync {
_forZenEmptyTab: tab.hasAttribute("zen-empty-tab"),
});
newTab.id = tab.id;
+ if (!tab.hasAttribute("pending")) {
+ newTab.removeAttribute("pending");
+ }
this.#syncItemWithOriginal(
tab,
newTab,
@@ -1489,18 +1492,29 @@ class nsZenWindowSync {
) {
return;
}
- let promise = this.#docShellSwitchPromise;
+ if (this.#docShellSwitchPromise) {
+ return;
+ }
+ const onTabSelect = event => {
+ if (event.detail?.previousTab === event.target) {
+ return;
+ }
+ this.#lastSelectedTab = null;
+ this.on_TabSelect(event, { ignorePromise: true });
+ };
this.#lastFocusedWindow = new WeakRef(window);
this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab);
+ window.addEventListener("TabSelect", onTabSelect, { once: true });
// eslint-disable-next-line no-async-promise-executor
this.#docShellSwitchPromise = new Promise(async resolve => {
- await promise;
await this.#onTabSwitchOrWindowFocus(window);
+ window.removeEventListener("TabSelect", onTabSelect);
resolve();
+ this.#docShellSwitchPromise = null;
});
}
- on_TabSelect(aEvent) {
+ on_TabSelect(aEvent, { ignorePromise = false } = {}) {
const tab = aEvent.target;
if (this.#lastSelectedTab?.deref() === tab) {
return;
@@ -1508,11 +1522,15 @@ class nsZenWindowSync {
this.#lastSelectedTab = new WeakRef(tab);
const previousTab = aEvent.detail.previousTab;
let promise = this.#docShellSwitchPromise;
+ if (promise && !ignorePromise) {
+ return;
+ }
// eslint-disable-next-line no-async-promise-executor
this.#docShellSwitchPromise = new Promise(async resolve => {
await promise;
await this.#onTabSwitchOrWindowFocus(tab.ownerGlobal, previousTab);
resolve();
+ this.#docShellSwitchPromise = null;
});
}
diff --git a/src/zen/spaces/ZenSpaceCreation.mjs b/src/zen/spaces/ZenSpaceCreation.mjs
index 18243b48d..26690eabf 100644
--- a/src/zen/spaces/ZenSpaceCreation.mjs
+++ b/src/zen/spaces/ZenSpaceCreation.mjs
@@ -148,6 +148,28 @@ class nsZenWorkspaceCreation extends MozXULElement {
this.createButton.disabled = !this.inputName.value.trim();
});
+ this.inputName.addEventListener("keydown", event => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ event.stopPropagation();
+ if (!this.createButton.disabled) {
+ this.createButton.doCommand();
+ }
+ }
+ });
+
+ // Bound on the root so Esc works regardless of which child has focus
+ // (name input, icon picker trigger, profile button, primary button).
+ // Open popups consume Esc before it reaches us, so the emoji/profile
+ // pickers still close as expected.
+ this.addEventListener("keydown", event => {
+ if (event.key === "Escape") {
+ event.preventDefault();
+ event.stopPropagation();
+ this.cancelButton.doCommand();
+ }
+ });
+
this.inputIcon.addEventListener("command", this.onIconCommand.bind(this));
this.profilesPopup = this.querySelector(
diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs
index a97dd0978..7b4792ab1 100644
--- a/src/zen/spaces/ZenSpaceManager.mjs
+++ b/src/zen/spaces/ZenSpaceManager.mjs
@@ -137,8 +137,6 @@ class nsZenWorkspaces {
document.documentElement.setAttribute("zen-private-window", "true");
}
- this.popupOpenHandler = this._popupOpenHandler.bind(this);
-
window.addEventListener("resize", this.onWindowResize.bind(this));
this.addPopupListeners();
@@ -599,16 +597,6 @@ class nsZenWorkspaces {
);
}
- _popupOpenHandler() {
- // If a popup is opened, we should stop the swipe gesture
- if (this._swipeManager?.isGestureActive) {
- document.documentElement.removeAttribute("swipe-gesture");
- gZenUIManager.tabsWrapper.style.removeProperty("scrollbar-width");
- this.updateTabsContainers();
- this._cancelSwipeAnimation();
- }
- }
-
get activeWorkspace() {
return this.#activeWorkspace;
}
@@ -1907,7 +1895,9 @@ class nsZenWorkspaces {
} = {}
) {
gZenUIManager.tabsWrapper.style.scrollbarWidth = "none";
- const kGlobalAnimationDuration = 0.2;
+ const kGlobalAnimationDuration =
+ Services.prefs.getIntPref("zen.workspaces.switch-animation-duration") /
+ 1000;
this._animatingChange = true;
const animations = [];
const workspaces = this.getWorkspaces();
diff --git a/src/zen/spaces/ZenSpacesSwipe.mjs b/src/zen/spaces/ZenSpacesSwipe.mjs
index fdffd64a5..7576efcc1 100644
--- a/src/zen/spaces/ZenSpacesSwipe.mjs
+++ b/src/zen/spaces/ZenSpacesSwipe.mjs
@@ -35,6 +35,19 @@ export class ZenSpacesSwipe {
}
this._attachWorkspaceSwipeGestures(element);
}
+
+ this._popupOpenHandler = this._popupOpenHandler.bind(this);
+ }
+
+ get #stripWidth() {
+ return (
+ window.windowUtils.getBoundsWithoutFlushing(
+ document.getElementById("navigator-toolbox")
+ ).width +
+ window.windowUtils.getBoundsWithoutFlushing(
+ document.getElementById("zen-sidebar-splitter")
+ ).width
+ );
}
_attachWorkspaceSwipeGestures(element) {
@@ -107,7 +120,7 @@ export class ZenSpacesSwipe {
gZenFolders.cancelPopupTimer();
document.documentElement.setAttribute("swipe-gesture", "true");
- document.addEventListener("popupshown", ws.popupOpenHandler, {
+ document.addEventListener("popupshown", this._popupOpenHandler, {
once: true,
});
@@ -128,17 +141,16 @@ export class ZenSpacesSwipe {
return;
}
+ const stripWidth = this.#stripWidth;
+
event.preventDefault();
event.stopPropagation();
- const stripWidth =
- window.windowUtils.getBoundsWithoutFlushing(
- document.getElementById("navigator-toolbox")
- ).width +
- window.windowUtils.getBoundsWithoutFlushing(
- document.getElementById("zen-sidebar-splitter")
- ).width;
- const delta = event.delta * stripWidth;
+ const delta =
+ event.delta *
+ Services.prefs.getIntPref(
+ "zen.workspaces.swipe-actions.delta-multiplier"
+ );
let translateX = this._swipeState.lastDelta + delta;
// Add a force multiplier as we are translating the strip depending on how close to the edge we are
let forceMultiplier = Math.min(
@@ -152,7 +164,8 @@ export class ZenSpacesSwipe {
translateX = this._swipeState.lastDelta;
}
- if (Math.abs(delta) > 0.8) {
+ if (Math.abs(delta) > 0.9) {
+ delete ws._hasAnimatedBackgrounds;
this._swipeState.direction = delta > 0 ? "left" : "right";
}
@@ -176,6 +189,10 @@ export class ZenSpacesSwipe {
const rawDirection = moveForward ? 1 : -1;
const direction = ws.naturalScroll ? -1 : 1;
await ws.changeWorkspaceShortcut(rawDirection * direction, true);
+ }
+
+ onSwipeGestureAnimationEnd() {
+ const ws = gZenWorkspaces;
// Reset swipe state
this._swipeState = {
@@ -183,10 +200,6 @@ export class ZenSpacesSwipe {
lastDelta: 0,
direction: null,
};
- }
-
- onSwipeGestureAnimationEnd() {
- const ws = gZenWorkspaces;
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", false);
document.documentElement.removeAttribute("swipe-gesture");
@@ -198,11 +211,15 @@ export class ZenSpacesSwipe {
);
delete ws._hasAnimatedBackgrounds;
ws.updateTabsContainers();
- document.removeEventListener("popupshown", ws.popupOpenHandler, {
+ document.removeEventListener("popupshown", this._popupOpenHandler, {
once: true,
});
}
+ _popupOpenHandler() {
+ this.onSwipeGestureAnimationEnd();
+ }
+
get isGestureActive() {
return this._swipeState?.isGestureActive;
}
diff --git a/src/zen/spaces/create-workspace-form.css b/src/zen/spaces/create-workspace-form.css
index de51a4737..566e81c93 100644
--- a/src/zen/spaces/create-workspace-form.css
+++ b/src/zen/spaces/create-workspace-form.css
@@ -138,6 +138,11 @@ zen-workspace-creation {
margin-left: auto;
min-width: unset !important;
border-radius: 6px;
+
+ &:focus-visible {
+ outline: 2px solid var(--zen-colors-border-contrast);
+ outline-offset: 2px;
+ }
}
}
@@ -153,6 +158,11 @@ zen-workspace-creation {
&:hover {
background-color: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1));
}
+
+ &:focus-visible {
+ outline: 2px solid var(--zen-colors-border-contrast);
+ outline-offset: 2px;
+ }
}
& .zen-workspace-creation-buttons {
diff --git a/src/zen/spaces/zen-workspaces.css b/src/zen/spaces/zen-workspaces.css
index f5d7cc796..56c1c0719 100644
--- a/src/zen/spaces/zen-workspaces.css
+++ b/src/zen/spaces/zen-workspaces.css
@@ -321,6 +321,7 @@ zen-workspace {
height: 100%;
overflow: hidden;
color: var(--toolbox-textcolor);
+ will-change: transform;
@media not (prefers-reduced-motion: reduce) {
transition: padding-top 0.1s;
diff --git a/src/zen/split-view/zen-split-view.css b/src/zen/split-view/zen-split-view.css
index 4027e4531..6e856e9db 100644
--- a/src/zen/split-view/zen-split-view.css
+++ b/src/zen/split-view/zen-split-view.css
@@ -238,7 +238,7 @@
width: 200px;
height: 250px;
border-radius: 16px;
- background: black;
+ background: light-dark(white, black);
justify-content: center;
align-items: center;
padding: 20px;
@@ -251,7 +251,7 @@
}
& label {
- color: white;
+ color: light-dark(black, white);
font-size: 14px;
font-weight: bold;
text-align: center;
diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs
index 38dfa9e68..893c89bb3 100644
--- a/src/zen/tabs/ZenPinnedTabManager.mjs
+++ b/src/zen/tabs/ZenPinnedTabManager.mjs
@@ -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) {
diff --git a/src/zen/tabs/zen-tabs/vertical-tabs-topbar.inc.css b/src/zen/tabs/zen-tabs/vertical-tabs-topbar.inc.css
index 3107480ea..35752d866 100644
--- a/src/zen/tabs/zen-tabs/vertical-tabs-topbar.inc.css
+++ b/src/zen/tabs/zen-tabs/vertical-tabs-topbar.inc.css
@@ -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;
diff --git a/src/zen/tabs/zen-tabs/vertical-tabs.css b/src/zen/tabs/zen-tabs/vertical-tabs.css
index 0118c3b8d..39d555fc2 100644
--- a/src/zen/tabs/zen-tabs/vertical-tabs.css
+++ b/src/zen/tabs/zen-tabs/vertical-tabs.css
@@ -318,7 +318,7 @@
&,
& .tab-content > image {
transition:
- scale 0.2s ease,
+ scale 0.1s ease,
var(--zen-tabbox-element-indent-transition);
}
}
@@ -1077,7 +1077,7 @@
#tabs-newtab-button {
max-height: var(--tab-min-height);
display: flex !important;
- transition: scale 0.2s ease;
+ transition: scale 0.1s ease;
#tabbrowser-tabs[movingtab] & {
transition: transform 0.1s ease;
}
@@ -1089,7 +1089,7 @@
&:active,
&[open] {
- scale: 0.99;
+ scale: 0.985;
}
&[in-urlbar] {
@@ -1131,6 +1131,7 @@
transition:
max-height 0.3s ease-out,
grid-template-columns 0.3s ease-out;
+ will-change: transform;
opacity: 1;
--min-essentials-width-wrap: calc(var(--tab-min-height) + 4px);
grid-template-columns: repeat(auto-fit, minmax(max(23.7%, var(--min-essentials-width-wrap)), 1fr));
diff --git a/src/zen/tests/live-folders/browser.toml b/src/zen/tests/live-folders/browser.toml
index bd75b3725..ccd76d2cb 100644
--- a/src/zen/tests/live-folders/browser.toml
+++ b/src/zen/tests/live-folders/browser.toml
@@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
[DEFAULT]
+prefs = ["zen.live-folders.github.skip-new-pr-ui-check=true"]
["browser_github_live_folder.js"]
diff --git a/src/zen/tests/live-folders/browser_github_live_folder.js b/src/zen/tests/live-folders/browser_github_live_folder.js
index ded5e5116..ee1237438 100644
--- a/src/zen/tests/live-folders/browser_github_live_folder.js
+++ b/src/zen/tests/live-folders/browser_github_live_folder.js
@@ -65,10 +65,10 @@ add_task(async function test_fetch_items_url_construction() {
const fetchedUrl = new URL(instance.fetch.firstCall.args[0]);
const searchParams = fetchedUrl.searchParams;
- Assert.ok(fetchedUrl.href.startsWith("https://github.com/issues/assigned"));
+ Assert.ok(fetchedUrl.href.startsWith("https://github.com/pulls"));
const query = searchParams.get("q");
- Assert.ok(query.includes("state:open"), "Should include state:open");
+ Assert.ok(query.includes("is:open"), "Should include state:open");
Assert.ok(query.includes("is:pr"), "Should include is:PR");
Assert.ok(query.includes("author:@me"), "Should include author:@me");
Assert.ok(!query.includes("assignee:@me"), "Should NOT include assignee:@me");
@@ -176,3 +176,169 @@ add_task(async function test_fetch_network_error() {
sandbox.restore();
});
+
+add_task(async function test_no_filter_enabled_returns_error() {
+ info(
+ "should short-circuit and return the no-filter error when every option is off"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = getGithubProviderForTest(sandbox, {
+ type: "pull-requests",
+ authorMe: false,
+ assignedMe: false,
+ reviewRequested: false,
+ });
+
+ const result = await instance.fetchItems();
+
+ Assert.equal(
+ result,
+ "zen-live-folder-github-no-filter",
+ "Should return the no-filter error id"
+ );
+ Assert.ok(
+ instance.fetch.notCalled,
+ "Should not issue a fetch when no filter is enabled"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_404_returns_no_auth() {
+ info("should treat a 404 as a missing-auth signal");
+
+ let sandbox = sinon.createSandbox();
+ let instance = getGithubProviderForTest(sandbox, {
+ type: "pull-requests",
+ authorMe: true,
+ });
+
+ instance.fetch.resolves({ status: 404, text: "" });
+
+ const result = await instance.fetchItems();
+
+ Assert.equal(
+ result,
+ "zen-live-folder-github-no-auth",
+ "Should return the no-auth error id"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_repo_excludes_emit_negative_repo_filters() {
+ info("should add -repo: clauses for each excluded repository");
+
+ let sandbox = sinon.createSandbox();
+ let instance = getGithubProviderForTest(sandbox, {
+ type: "pull-requests",
+ authorMe: true,
+ assignedMe: false,
+ reviewRequested: false,
+ repoExcludes: ["zen-browser/desktop", "foo/bar"],
+ });
+
+ instance.fetch.resolves({ status: 200, text: "" });
+
+ await instance.fetchItems();
+
+ const fetchedUrl = new URL(instance.fetch.firstCall.args[0]);
+ const query = fetchedUrl.searchParams.get("q");
+
+ Assert.ok(
+ query.includes("-repo:zen-browser/desktop"),
+ "Should exclude zen-browser/desktop from the query"
+ );
+ Assert.ok(
+ query.includes("-repo:foo/bar"),
+ "Should exclude foo/bar from the query"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_pull_requests_json_api_parsing() {
+ info("should parse the new PR dashboard JSON payload");
+
+ let sandbox = sinon.createSandbox();
+ let instance = getGithubProviderForTest(sandbox, {
+ type: "pull-requests",
+ authorMe: true,
+ });
+
+ const payload = JSON.stringify({
+ payload: {
+ pullsDashboardSurfaceContentRoute: {
+ results: [
+ {
+ repoNameWithOwner: "zen-browser/desktop",
+ number: 42,
+ title: "Add live folders",
+ author: { displayLogin: "alice" },
+ permalink: "https://github.com/zen-browser/desktop/pull/42",
+ },
+ {
+ repoNameWithOwner: "zen-browser/desktop",
+ number: 43,
+ title: "Fix bug",
+ author: { displayLogin: "bob" },
+ permalink: "https://github.com/zen-browser/desktop/pull/43",
+ },
+ ],
+ },
+ },
+ });
+
+ instance.fetch.resolves({ status: 200, text: payload });
+
+ const items = await instance.fetchItems();
+
+ Assert.equal(items.length, 2, "Should parse two PRs from the JSON payload");
+ Assert.equal(items[0].id, "zen-browser/desktop#42");
+ Assert.equal(items[0].title, "Add live folders");
+ Assert.equal(items[0].subtitle, "alice");
+ Assert.equal(items[0].url, "https://github.com/zen-browser/desktop/pull/42");
+ Assert.equal(items[1].id, "zen-browser/desktop#43");
+ Assert.ok(
+ instance.state.isJsonApi,
+ "Should mark the provider as using the JSON API"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_pull_requests_json_api_falls_back_to_html() {
+ info(
+ "should fall back to HTML parsing when an HTML response arrives unexpectedly"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = getGithubProviderForTest(sandbox, {
+ type: "pull-requests",
+ authorMe: true,
+ });
+
+ // Simulate a previous fetch having locked the provider into JSON-API mode.
+ instance.state.isJsonApi = true;
+
+ instance.fetch.resolves({
+ status: 200,
+ text: "not JSON",
+ });
+
+ const result = await instance.fetchItems();
+
+ Assert.equal(
+ instance.state.isJsonApi,
+ false,
+ "Should clear isJsonApi after seeing a non-JSON response"
+ );
+ Assert.equal(
+ result,
+ "zen-live-folder-failed-fetch",
+ "Should surface a fetch error so the user is prompted to retry"
+ );
+
+ sandbox.restore();
+});
diff --git a/src/zen/tests/live-folders/browser_rss_live_folder.js b/src/zen/tests/live-folders/browser_rss_live_folder.js
index aef39b9da..7b28a8f48 100644
--- a/src/zen/tests/live-folders/browser_rss_live_folder.js
+++ b/src/zen/tests/live-folders/browser_rss_live_folder.js
@@ -162,9 +162,9 @@ add_task(async function test_max_items_limit() {
const rssXml = `
- - 11${date}
- - 22${date}
- - 33${date}
+ - 1https://example.com/1${date}
+ - 2https://example.com/2${date}
+ - 3https://example.com/3${date}
`;
@@ -218,6 +218,113 @@ add_task(async function test_invalid_dates() {
sandbox.restore();
});
+add_task(async function test_item_url_scheme_filtering() {
+ info("should drop items whose link uses a non-http(s) scheme");
+
+ let sandbox = sinon.createSandbox();
+ let instance = getRssProviderForTest(sandbox, { timeRange: 0 });
+
+ const date = new Date().toUTCString();
+ const rssXml = `
+
+
+ -
+ JavaScript scheme
+ javascript:alert(1)
+ ${date}
+
+ -
+ Data scheme
+ data:text/html,<script>alert(1)</script>
+ ${date}
+
+ -
+ File scheme
+ file:///etc/passwd
+ ${date}
+
+ -
+ about: scheme
+ about:config
+ ${date}
+
+ -
+ chrome: scheme
+ chrome://browser/content/browser.xhtml
+ ${date}
+
+ -
+ Invalid URL
+ not a url
+ ${date}
+
+ -
+ Good https
+ https://example.com/good
+ ${date}
+
+ -
+ Good http
+ http://example.com/good
+ ${date}
+
+
+
+ `;
+
+ instance.fetch.resolves({ text: rssXml });
+
+ const items = await instance.fetchItems();
+
+ Assert.equal(
+ items.length,
+ 2,
+ "Only http(s) items should survive scheme filtering"
+ );
+ Assert.deepEqual(
+ items.map(i => i.url).sort(),
+ ["http://example.com/good", "https://example.com/good"],
+ "Surviving items should be the http and https links"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_atom_item_url_scheme_filtering() {
+ info("should drop Atom entries whose link href uses a non-http(s) scheme");
+
+ let sandbox = sinon.createSandbox();
+ let instance = getRssProviderForTest(sandbox, { timeRange: 0 });
+
+ const updated = new Date().toISOString();
+ const atomXml = `
+
+ Atom Feed
+
+ Bad scheme
+
+ urn:uuid:bad
+ ${updated}
+
+
+ Good scheme
+
+ urn:uuid:good
+ ${updated}
+
+
+ `;
+
+ instance.fetch.resolves({ text: atomXml });
+
+ const items = await instance.fetchItems();
+
+ Assert.equal(items.length, 1, "Only the https Atom entry should remain");
+ Assert.equal(items[0].url, "https://example.com/atom-good");
+
+ sandbox.restore();
+});
+
add_task(async function test_fetch_network_error() {
info("should return empty array on network error");
diff --git a/src/zen/tests/ub-actions/browser.toml b/src/zen/tests/ub-actions/browser.toml
index b0915f7ac..c0e864bd5 100644
--- a/src/zen/tests/ub-actions/browser.toml
+++ b/src/zen/tests/ub-actions/browser.toml
@@ -5,3 +5,4 @@
[DEFAULT]
["browser_ub_actions_search.js"]
+["browser_workspace_restrict_search.js"]
diff --git a/src/zen/tests/ub-actions/browser_workspace_restrict_search.js b/src/zen/tests/ub-actions/browser_workspace_restrict_search.js
new file mode 100644
index 000000000..27adf48eb
--- /dev/null
+++ b/src/zen/tests/ub-actions/browser_workspace_restrict_search.js
@@ -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"
+ );
+});
diff --git a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs
index 8adf50107..5fb7d1c4c 100644
--- a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs
+++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs
@@ -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;
}
diff --git a/surfer.json b/surfer.json
index d5b3dfcdd..7a146831c 100644
--- a/surfer.json
+++ b/surfer.json
@@ -5,8 +5,8 @@
"binaryName": "zen",
"version": {
"product": "firefox",
- "version": "150.0",
- "candidate": "150.0",
+ "version": "150.0.2",
+ "candidate": "150.0.2",
"candidateBuild": 1
},
"buildOptions": {
@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
- "displayVersion": "1.19.10b",
+ "displayVersion": "1.19.12b",
"github": {
"repo": "zen-browser/desktop"
},
diff --git a/tools/ffprefs/src/main.rs b/tools/ffprefs/src/main.rs
index ac11c8e5b..d58b0ab0f 100644
--- a/tools/ffprefs/src/main.rs
+++ b/tools/ffprefs/src/main.rs
@@ -107,9 +107,9 @@ use std::env;
use std::fs;
use std::path::PathBuf;
-const STATIC_PREFS: &str = "../engine/modules/libpref/init/zen-static-prefs.inc";
-const FIREFOX_PREFS: &str = "../engine/browser/app/profile/firefox.js";
-const DYNAMIC_PREFS: &str = "../engine/browser/app/profile/zen.js";
+const STATIC_PREFS: &str = "modules/libpref/init/zen-static-prefs.inc";
+const FIREFOX_PREFS: &str = "browser/app/profile/firefox.js";
+const DYNAMIC_PREFS: &str = "browser/app/profile/zen.js";
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Preference {
@@ -124,12 +124,6 @@ struct Preference {
sticky: Option,
}
-fn get_config_path() -> PathBuf {
- let mut path = env::current_dir().expect("Failed to get current directory");
- path.push("prefs");
- path
-}
-
fn ordered_prefs(mut prefs: Vec) -> Vec {
// Sort preferences by name
prefs.sort_by(|a, b| a.name.cmp(&b.name));
@@ -153,11 +147,10 @@ fn get_prefs_files_recursively(dir: &PathBuf, files: &mut Vec) {
}
}
-fn load_preferences() -> Vec {
+fn load_preferences(prefs_path: &PathBuf) -> Vec {
let mut prefs = Vec::new();
- let config_path = get_config_path();
let mut pref_files = Vec::new();
- get_prefs_files_recursively(&config_path, &mut pref_files);
+ get_prefs_files_recursively(&prefs_path, &mut pref_files);
for file_path in pref_files {
let content = fs::read_to_string(&file_path).expect("Failed to read file");
let mut parsed_prefs: Vec =
@@ -263,14 +256,9 @@ fn get_value(pref: &Preference) -> String {
}
}
-fn write_preferences(prefs: &[Preference]) {
- let config_path = get_config_path();
- if !config_path.exists() {
- fs::create_dir_all(&config_path).expect("Failed to create prefs directory");
- }
-
- let static_prefs_path = config_path.join(STATIC_PREFS);
- let dynamic_prefs_path = config_path.join(DYNAMIC_PREFS);
+fn write_preferences(engine_path: &PathBuf, prefs: &[Preference]) {
+ let static_prefs_path = engine_path.join(STATIC_PREFS);
+ let dynamic_prefs_path = engine_path.join(DYNAMIC_PREFS);
println!(
"Writing preferences to:\n Static: {}\n Dynamic: {}",
static_prefs_path.display(),
@@ -300,10 +288,10 @@ fn write_preferences(prefs: &[Preference]) {
fs::write(&dynamic_prefs_path, dynamic_content).expect("Failed to write dynamic prefs");
}
-fn prepare_zen_prefs() {
+fn prepare_zen_prefs(engine_path: &PathBuf) {
// Add `#include zen.js` to the bottom of the firefox.js file if it doesn't exist
let line = "#include zen.js";
- let firefox_prefs_path = get_config_path().join(FIREFOX_PREFS);
+ let firefox_prefs_path = engine_path.join(FIREFOX_PREFS);
if let Ok(mut content) = fs::read_to_string(&firefox_prefs_path) {
if !content.contains(line) {
content.push_str(format!("\n{}\n", line).as_str());
@@ -351,15 +339,19 @@ fn expand_pref_values(prefs: &mut [Preference]) {
fn main() {
let args: Vec = env::args().collect();
- let root_path = if args.len() > 1 {
+ let prefs_path = if args.len() > 1 {
PathBuf::from(&args[1])
} else {
- env::current_dir().expect("Failed to get current directory")
+ PathBuf::from("prefs")
+ };
+ let engine_path = if args.len() > 2 {
+ PathBuf::from(&args[2])
+ } else {
+ PathBuf::from("engine")
};
- env::set_current_dir(&root_path).expect("Failed to change directory");
- prepare_zen_prefs();
- let mut preferences = load_preferences();
+ prepare_zen_prefs(&engine_path);
+ let mut preferences = load_preferences(&prefs_path);
expand_pref_values(&mut preferences);
- write_preferences(&preferences);
+ write_preferences(&engine_path, &preferences);
}