From b295d4a0d0d25848211c43cc2e7068859c2cb9ea Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 30 Apr 2020 06:54:51 -0400 Subject: [PATCH] Make the new "fenix" popup panel the default one The old "classic" popup panel will still be used when at least one of the following is true: - advanced setting `uiFlavor` is set to `classic`; or - the browser is Chromium 65 or older; or - the browser is Firefox 67 or older The default configuration of the new popup panel at installation time is to show the power button, statistics and the basic tool icons, i.e. access to dashboard, logger, pickers. For existing installations, the new popup panel will be configured by respecting the existing configuration of the classic one. The new popup panel is currently already in use on Firefox for Android, and the visual redesign was made according to suggestions and feedback from to be optimal for Firefox for Android. The new popup panel will allow closing the following pending issues: - https://github.com/uBlockOrigin/uBlock-issues/issues/255 - https://github.com/uBlockOrigin/uBlock-issues/issues/178 --- platform/chromium/manifest.json | 2 +- platform/firefox/manifest.json | 2 +- platform/opera/manifest.json | 2 +- src/_locales/en/messages.json | 10 ++- src/css/popup-fenix.css | 72 ++++++++++++++------- src/css/themes/default.css | 2 + src/js/background.js | 2 + src/js/messaging.js | 12 ++-- src/js/popup-fenix.js | 109 +++++++++++++++++++++----------- src/js/popup.js | 21 +++--- src/js/start.js | 27 +++++--- src/js/ublock.js | 2 +- src/popup-fenix.html | 37 +++++++---- 13 files changed, 196 insertions(+), 104 deletions(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index d9559c9ce..0b624c1d3 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -9,7 +9,7 @@ "32": "img/icon_32.png" }, "default_title": "uBlock Origin", - "default_popup": "popup.html" + "default_popup": "popup-fenix.html" }, "commands": { "launch-element-zapper": { diff --git a/platform/firefox/manifest.json b/platform/firefox/manifest.json index e47fbebe5..0ebc4a5ef 100644 --- a/platform/firefox/manifest.json +++ b/platform/firefox/manifest.json @@ -10,7 +10,7 @@ "32": "img/icon_32.png" }, "default_title": "uBlock Origin", - "default_popup": "popup.html" + "default_popup": "popup-fenix.html" }, "browser_specific_settings": { "gecko": { diff --git a/platform/opera/manifest.json b/platform/opera/manifest.json index 91400fcb8..649acd810 100644 --- a/platform/opera/manifest.json +++ b/platform/opera/manifest.json @@ -8,7 +8,7 @@ "16": "img/icon_16.png", "32": "img/icon_32.png" }, - "default_popup": "popup.html", + "default_popup": "popup-fenix.html", "default_title": "uBlock Origin" }, "commands": { diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 9bcd6c43e..648a6714d 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -201,7 +201,11 @@ }, "popupMoreButton_v2":{ "message":"More", - "description":"Label to be used to toggle overview panel" + "description":"Label to be used to show popup panel sections" + }, + "popupLessButton_v2":{ + "message":"Less", + "description":"Label to be used to hide popup panel sections" }, "popupTipGlobalRules":{ "message":"Global rules: this column is for rules which apply to all sites.", @@ -259,6 +263,10 @@ "message":"{{count}} out of {{total}}", "description":"appears in popup" }, + "popupVersion":{ + "message":"Version", + "description":"Example of use: Version 1.26.4" + }, "pickerCreate":{ "message":"Create", "description":"English: Create" diff --git a/src/css/popup-fenix.css b/src/css/popup-fenix.css index 9c7b5f071..fe490ace9 100644 --- a/src/css/popup-fenix.css +++ b/src/css/popup-fenix.css @@ -56,11 +56,11 @@ hr { align-items: stretch; display: flex; justify-content: space-between; - margin: var(--popup-default-gap) 0.5em; + margin: 0.5em 0.5em var(--popup-default-gap) 0.5em; } #switch { display: flex; - flex-grow: 2; + flex-grow: 3; justify-content: center; } #switch .fa-icon { @@ -71,10 +71,6 @@ hr { margin: 0; padding: 0; } -#switch .fa-icon:hover { - color: var(--popup-power-ink-hover); - fill: var(--popup-power-ink-hover); - } body.off #switch .fa-icon { color: var(--fg-0-20); fill: var(--fg-0-20); @@ -85,7 +81,7 @@ body.off #switch .fa-icon { box-sizing: border-box; display: flex; flex-direction: column; - flex-grow: 1; + flex-grow: 2; justify-content: space-evenly; } .rulesetTools [id] { @@ -93,7 +89,7 @@ body.off #switch .fa-icon { border: 1px solid #ddc; border-radius: 4px; cursor: pointer; - fill: #888; + fill: var(--default-ink-a50); flex-grow: 1; font-size: 2.2em; padding: 0; @@ -177,9 +173,23 @@ body.mobile.no-tooltips .toolRibbon .tool { visibility: visible; } -body:not(.dfEnabled) #moreButton .fa-icon { +.moreOrLess > span { + cursor: pointer; + } +#moreButton .fa-icon { transform: rotate(180deg); } +#lessButton { + text-align: right; + } +body[data-more="a b c d e"] #moreButton { + pointer-events: none; + visibility: hidden; + } +body[data-more=""] #lessButton { + pointer-events: none; + visibility: hidden; + } #tooltip { background-color: var(--bg-tooltip); @@ -224,9 +234,6 @@ body[dir="rtl"] #tooltip { text-align: right; --rule-cell-width: 5em; } -body:not(.dfEnabled) #firewall { - display: none; - } #firewall > div { border: 0; direction: ltr; @@ -246,11 +253,11 @@ body:not(.dfEnabled) #firewall { #firewall.expanded > div.isSubDomain.expandException:not(.isRootContext) { display: none; } -#firewall > div > span { +#firewall > div > span, +#actionSelector > #dynaCounts { background-color: var(--bg-popup-cell-2); border: none; box-sizing: border-box; - -moz-box-sizing: border-box; display: inline-flex; padding: 0.4em 0; position: relative; @@ -313,7 +320,8 @@ body:not(.dfEnabled) #firewall { #firewall.expanded > div:not(.expandException) > span:nth-of-type(3), #firewall:not(.expanded) > div.expandException > span:nth-of-type(3), #firewall:not(.expanded) > div.isDomain:not(.expandException) > span:nth-of-type(4), -#firewall.expanded > div.isDomain.expandException > span:nth-of-type(4) { +#firewall.expanded > div.isDomain.expandException > span:nth-of-type(4), +#actionSelector > #dynaCounts { display: inline-flex; justify-content: space-between; } @@ -423,15 +431,15 @@ body.advancedUser #firewall > div > span.ownRule, color: var(--default-surface); } body.advancedUser #firewall > div > span.allowRule.ownRule, -#actionSelector > #dynaAllow:hover { +:root.desktop #actionSelector > #dynaAllow:hover { background-color: var(--bg-popup-cell-allow-own); } body.advancedUser #firewall > div > span.blockRule.ownRule, -#actionSelector > #dynaBlock:hover { +:root.desktop #actionSelector > #dynaBlock:hover { background-color: var(--bg-popup-cell-block-own); } body.advancedUser #firewall > div > span.noopRule.ownRule, -#actionSelector > #dynaNoop:hover { +:root.desktop #actionSelector > #dynaNoop:hover { background-color: var(--bg-popup-cell-noop-own); } @@ -458,11 +466,8 @@ body.advancedUser #firewall > div > span.noopRule.ownRule, width: 33.5%; } #actionSelector > #dynaCounts { - align-items: center; background-color: transparent; - display: inline-flex; height: 100%; - justify-content: space-between; left: 0; pointer-events: none; position: absolute; @@ -488,6 +493,19 @@ body.advancedUser #firewall > div > span.noopRule.ownRule, :root body[data-ui~="+no-popups"] #no-popups { display: flex; } +body:not([data-more~="a"]) [data-more="a"], +body:not([data-more~="b"]) [data-more="b"], +body:not([data-more~="c"]) [data-more="c"], +body:not([data-more~="e"]) [data-more="e"] { + height: 0; + margin-bottom: 0; + margin-top: 0; + overflow-y: hidden; + visibility: hidden; + } +body:not([data-more~="d"]) [data-more="d"] { + display: none; + } /* mouse-driven devices */ :root.desktop { @@ -505,6 +523,13 @@ body.advancedUser #firewall > div > span.noopRule.ownRule, max-width: 300px; width: max-content; } +:root.desktop #switch .fa-icon:hover { + color: var(--popup-power-ink-hover); + fill: var(--popup-power-ink-hover); + } +:root.desktop .rulesetTools [id]:hover { + fill: var(--default-ink); + } :root.desktop #firewall { direction: rtl; flex-grow: 1; @@ -513,7 +538,7 @@ body.advancedUser #firewall > div > span.noopRule.ownRule, max-height: max(100vh, 600px); min-width: 360px; overflow-y: auto; - width: max-content; + width: min-content; } :root.desktop .tool { padding: 0.5em; @@ -521,3 +546,6 @@ body.advancedUser #firewall > div > span.noopRule.ownRule, :root.desktop .tool:hover { background-color: var(--button-surface); } +:root.desktop .moreOrLess > span:hover { + background-color: var(--button-surface); + } diff --git a/src/css/themes/default.css b/src/css/themes/default.css index 67542c030..44caba025 100644 --- a/src/css/themes/default.css +++ b/src/css/themes/default.css @@ -18,6 +18,7 @@ --ink-50: #291d4f; --ink-80: #20123a; --ink-80-a4: #20123a0a; + --ink-80-a50: #20123a88; --ink-90: #1d1133; --light-gray-10: #f9f9fb; --light-gray-30: #e0e0e6; @@ -53,6 +54,7 @@ --default-ink: var(--ink-80); --default-ink-a4: var(--ink-80-a4); + --default-ink-a50: var(--ink-80-a50); --default-surface: var(--light-gray-10); --bg-1: hsla(240, 20%, 98%, 1); diff --git a/src/js/background.js b/src/js/background.js index febc58d33..1ab8ff0a0 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -92,6 +92,8 @@ const µBlock = (( ) => { // jshint ignore:line ignoreGenericCosmeticFilters: vAPI.webextFlavor.soup.has('mobile'), largeMediaSize: 50, parseAllABPHideFilters: true, + popupPanelSections: 0b111, + popupPanelDisabledSections: 0, prefetchingDisabled: true, requestLogMaxEntries: 1000, showIconBadge: true, diff --git a/src/js/messaging.js b/src/js/messaging.js index 1561346bd..cb7e580f1 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -264,14 +264,14 @@ const getFirewallRules = function(srcHostname, desHostnames) { const popupDataFromTabId = function(tabId, tabTitle) { const tabContext = µb.tabContextManager.mustLookup(tabId); const rootHostname = tabContext.rootHostname; + const µbus = µb.userSettings; const r = { - advancedUserEnabled: µb.userSettings.advancedUserEnabled, + advancedUserEnabled: µbus.advancedUserEnabled, appName: vAPI.app.name, appVersion: vAPI.app.version, - colorBlindFriendly: µb.userSettings.colorBlindFriendly, + colorBlindFriendly: µbus.colorBlindFriendly, cosmeticFilteringSwitch: false, - dfEnabled: µb.userSettings.dynamicFilteringEnabled, - firewallPaneMinimized: µb.userSettings.firewallPaneMinimized, + firewallPaneMinimized: µbus.firewallPaneMinimized, globalAllowedRequestCount: µb.localSettings.allowedRequestCount, globalBlockedRequestCount: µb.localSettings.blockedRequestCount, fontSize: µb.hiddenSettings.popupFontSize, @@ -283,9 +283,11 @@ const popupDataFromTabId = function(tabId, tabTitle) { pageAllowedRequestCount: 0, pageBlockedRequestCount: 0, popupBlockedCount: 0, + popupPanelSections: µbus.popupPanelSections, + popupPanelDisabledSections: µbus.popupPanelDisabledSections, tabId: tabId, tabTitle: tabTitle, - tooltipsDisabled: µb.userSettings.tooltipsDisabled + tooltipsDisabled: µbus.tooltipsDisabled }; if ( µb.hiddenSettings.uiPopupConfig !== 'undocumented' ) { diff --git a/src/js/popup-fenix.js b/src/js/popup-fenix.js index 018c62d89..55774be0c 100644 --- a/src/js/popup-fenix.js +++ b/src/js/popup-fenix.js @@ -30,22 +30,21 @@ /******************************************************************************/ +/* let popupFontSize; vAPI.localStorage.getItemAsync('popupFontSize').then(value => { if ( typeof value !== 'string' || value === 'unset' ) { return; } document.body.style.setProperty('font-size', value); popupFontSize = value; }); +*/ // https://github.com/chrisaljoudi/uBlock/issues/996 // Experimental: mitigate glitchy popup UI: immediately set the firewall // pane visibility to its last known state. By default the pane is hidden. -let dfPaneVisibleStored; -vAPI.localStorage.getItemAsync('popupFirewallPane').then(value => { - dfPaneVisibleStored = value === true || value === 'true'; - if ( dfPaneVisibleStored ) { - document.body.classList.add('dfEnabled'); - } +vAPI.localStorage.getItemAsync('popupPanelSections').then(bits => { + if ( typeof bits !== 'number' ) { return; } + sectionBitsToAttribute(bits); }); /******************************************************************************/ @@ -485,21 +484,6 @@ const renderPopup = function() { uDom.nodeFromSelector('#no-remote-fonts .fa-icon-badge') .textContent = total ? Math.min(total, 99).toLocaleString() : ''; - // https://github.com/chrisaljoudi/uBlock/issues/470 - // This must be done here, to be sure the popup is resized properly - const dfPaneVisible = popupData.dfEnabled; - - // https://github.com/chrisaljoudi/uBlock/issues/1068 - // Remember the last state of the firewall pane. This allows to - // configure the popup size early next time it is opened, which means a - // less glitchy popup at open time. - if ( dfPaneVisible !== dfPaneVisibleStored ) { - dfPaneVisibleStored = dfPaneVisible; - vAPI.localStorage.setItem('popupFirewallPane', dfPaneVisibleStored); - } - - body.classList.toggle('dfEnabled', dfPaneVisible === true); - document.documentElement.classList.toggle( 'colorBlind', popupData.colorBlindFriendly === true @@ -508,7 +492,7 @@ const renderPopup = function() { setGlobalExpand(popupData.firewallPaneMinimized === false, true); // Build dynamic filtering pane only if in use - if ( dfPaneVisible ) { + if ( (popupData.popupPanelSections & ~popupData.popupPanelDisabledSections & 0b1000) !== 0 ) { buildAllFirewallRows(); } @@ -588,6 +572,7 @@ let renderOnce = function() { const body = document.body; +/* if ( popupData.fontSize !== popupFontSize ) { popupFontSize = popupData.fontSize; if ( popupFontSize !== 'unset' ) { @@ -598,11 +583,13 @@ let renderOnce = function() { vAPI.localStorage.removeItem('popupFontSize'); } } +*/ - // https://github.com/uBlockOrigin/uBlock-issues/issues/22 - if ( popupData.advancedUserEnabled !== true ) { - uDom('#firewall [title][data-src]').removeAttr('title'); - } + uDom.nodeFromId('version').textContent = popupData.appVersion; + + sectionBitsToAttribute( + popupData.popupPanelSections & ~popupData.popupPanelDisabledSections + ); if ( popupData.uiPopupConfig !== undefined ) { document.body.setAttribute('data-ui', popupData.uiPopupConfig); @@ -612,6 +599,11 @@ let renderOnce = function() { if ( popupData.tooltipsDisabled === true ) { uDom('[title]').removeAttr('title'); } + + // https://github.com/uBlockOrigin/uBlock-issues/issues/22 + if ( popupData.advancedUserEnabled !== true ) { + uDom('#firewall [title][data-src]').removeAttr('title'); + } }; /******************************************************************************/ @@ -733,29 +725,71 @@ const gotoURL = function(ev) { /******************************************************************************/ -const toggleFirewallPane = function() { - popupData.dfEnabled = !popupData.dfEnabled; +// The popup panel is made of sections. Visiblity of sections can +// be toggle on/off. +const maxNumberOfSections = 5; + +const sectionBitsFromAttribute = function() { + const attr = document.body.dataset.more; + if ( attr === '' ) { return 0; } + let bits = 0; + for ( const c of attr.split(' ') ) { + bits |= 1 << (c.charCodeAt(0) - 97); + } + return bits; +}; + +const sectionBitsToAttribute = function(bits) { + const attr = []; + for ( let i = 0; i < maxNumberOfSections; i++ ) { + const bit = 1 << i; + if ( (bits & bit) === 0 ) { continue; } + attr.push(String.fromCharCode(97 + i)); + } + document.body.dataset.more = attr.join(' '); +}; + +const toggleSections = function(more) { + const mask = ~popupData.popupPanelDisabledSections; + let currentBits = sectionBitsFromAttribute(); + let newBits = currentBits; + for ( let i = 0; i < maxNumberOfSections; i++ ) { + const bit = 1 << (more ? i : maxNumberOfSections - i - 1); + if ( (mask & bit) === 0 ) { continue; } + if ( more ) { + newBits |= bit; + } else { + newBits &= ~bit; + } + if ( newBits !== currentBits ) { break; } + } + if ( newBits === currentBits ) { return; } + + sectionBitsToAttribute(newBits); + + popupData.popupPanelSections = newBits; messaging.send('popupPanel', { what: 'userSettings', - name: 'dynamicFilteringEnabled', - value: popupData.dfEnabled, + name: 'popupPanelSections', + value: newBits, }); // https://github.com/chrisaljoudi/uBlock/issues/996 - // Remember the last state of the firewall pane. This allows to - // configure the popup size early next time it is opened, which means a - // less glitchy popup at open time. - dfPaneVisibleStored = popupData.dfEnabled; - vAPI.localStorage.setItem('popupFirewallPane', dfPaneVisibleStored); + // Remember the last state of the firewall pane. This allows to + // configure the popup size early next time it is opened, which means a + // less glitchy popup at open time. + vAPI.localStorage.setItem('popupPanelSections', newBits); // Dynamic filtering pane may not have been built yet - document.body.classList.toggle('dfEnabled', popupData.dfEnabled); - if ( popupData.dfEnabled && dfPaneBuilt === false ) { + if ( (newBits & 0b1000) !== 0 && dfPaneBuilt === false ) { buildAllFirewallRows(); } }; +uDom('#moreButton').on('click', ( ) => { toggleSections(true); }); +uDom('#lessButton').on('click', ( ) => { toggleSections(false); }); + /******************************************************************************/ const mouseenterCellHandler = function() { @@ -1138,7 +1172,6 @@ const getPopupData = async function(tabId) { uDom('#switch').on('click', toggleNetFilteringSwitch); uDom('#gotoZap').on('click', gotoZap); uDom('#gotoPick').on('click', gotoPick); -uDom('#moreButton').on('click', toggleFirewallPane); uDom('.hnSwitch').on('click', ev => { toggleHostnameSwitch(ev); }); uDom('#saveRules').on('click', saveFirewallRules); uDom('#revertRules').on('click', ( ) => { revertFirewallRules(); }); diff --git a/src/js/popup.js b/src/js/popup.js index 266036a4d..3be9676c0 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -506,7 +506,7 @@ const renderPopup = function() { // https://github.com/chrisaljoudi/uBlock/issues/470 // This must be done here, to be sure the popup is resized properly - const dfPaneVisible = popupData.dfEnabled; + const dfPaneVisible = (popupData.popupPanelSections & 0b1000) !== 0; // https://github.com/chrisaljoudi/uBlock/issues/1068 // Remember the last state of the firewall pane. This allows to @@ -517,10 +517,7 @@ const renderPopup = function() { vAPI.localStorage.setItem('popupFirewallPane', dfPaneVisibleStored); } - uDom.nodeFromId('panes').classList.toggle( - 'dfEnabled', - dfPaneVisible === true - ); + uDom.nodeFromId('panes').classList.toggle('dfEnabled', dfPaneVisible === true); document.documentElement.classList.toggle( 'colorBlind', @@ -795,24 +792,24 @@ const gotoURL = function(ev) { /******************************************************************************/ const toggleFirewallPane = function() { - popupData.dfEnabled = !popupData.dfEnabled; + popupData.popupPanelSections = popupData.popupPanelSections ^ 0b1000; messaging.send('popupPanel', { what: 'userSettings', - name: 'dynamicFilteringEnabled', - value: popupData.dfEnabled, + name: 'popupPanelSections', + value: popupData.popupPanelSections | 0b0111, }); // https://github.com/chrisaljoudi/uBlock/issues/996 // Remember the last state of the firewall pane. This allows to // configure the popup size early next time it is opened, which means a // less glitchy popup at open time. - dfPaneVisibleStored = popupData.dfEnabled; + dfPaneVisibleStored = (popupData.popupPanelSections & 0b1000) !== 0; vAPI.localStorage.setItem('popupFirewallPane', dfPaneVisibleStored); // Dynamic filtering pane may not have been built yet - uDom.nodeFromId('panes').classList.toggle('dfEnabled', popupData.dfEnabled); - if ( popupData.dfEnabled && dfPaneBuilt === false ) { + uDom.nodeFromId('panes').classList.toggle('dfEnabled', dfPaneVisibleStored); + if ( dfPaneVisibleStored && dfPaneBuilt === false ) { buildAllFirewallRows(); } }; @@ -1076,7 +1073,7 @@ const toggleHostnameSwitch = async function(ev) { hostname: popupData.pageHostname, state: target.classList.contains('on'), tabId: popupData.tabId, - persist: popupData.dfEnabled === false || ev.ctrlKey || ev.metaKey, + persist: (popupData.popupPanelSections & 0b1000) === 0 || ev.ctrlKey || ev.metaKey, }); cachePopupData(response); diff --git a/src/js/start.js b/src/js/start.js index 79f8b3f96..7e2b3f74a 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -114,6 +114,15 @@ const onVersionReady = function(lastVersion) { µb.saveHostnameSwitches(); } + // Configure new popup panel according to classic popup panel + // configuration. + if ( lastVersionInt !== 0 && lastVersionInt <= 1026003014 ) { + µb.userSettings.popupPanelSections = + µb.userSettings.dynamicFilteringEnabled === true ? 0b1111 : 0b0111; + µb.userSettings.dynamicFilteringEnabled = undefined; + µb.saveUserSettings(); + } + vAPI.storage.set({ version: vAPI.app.version }); }; @@ -356,14 +365,16 @@ if ( browser.browserAction instanceof Object && browser.browserAction.setPopup instanceof Function ) { - let uiFlavor = µb.hiddenSettings.uiFlavor; - if ( uiFlavor === 'unset' && vAPI.webextFlavor.soup.has('mobile') ) { - uiFlavor = 'fenix'; - } - if ( uiFlavor !== 'unset' && /\w+/.test(uiFlavor) ) { - browser.browserAction.setPopup({ - popup: vAPI.getURL(`popup-${uiFlavor}.html`) - }); + const env = vAPI.webextFlavor; + if ( + µb.hiddenSettings.uiFlavor === 'classic' || ( + µb.hiddenSettings.uiFlavor === 'unset' && ( + env.soup.has('chromium') && env.major < 66 || + env.soup.has('firefox') && env.major < 68 + ) + ) + ) { + browser.browserAction.setPopup({ popup: vAPI.getURL('popup.html') }); } } diff --git a/src/js/ublock.js b/src/js/ublock.js index 6dd2a792c..79dc7fd52 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -331,7 +331,7 @@ const matchBucket = function(url, hostname, bucket, start) { switch ( name ) { case 'advancedUserEnabled': if ( value === true ) { - us.dynamicFilteringEnabled = true; + us.popupPanelSections = 0b1111; } break; case 'autoUpdate': diff --git a/src/popup-fenix.html b/src/popup-fenix.html index 0c080b4bf..6fd77438b 100644 --- a/src/popup-fenix.html +++ b/src/popup-fenix.html @@ -11,7 +11,7 @@ - +
@@ -27,35 +27,44 @@
ephemeralnewyork.­wordpress.com
-
-
- - - -
-
-
+
+
ph-popups film eye-slash ph-readermode-text-size code
-
-
+
+
+ + + +
+
+
bolt eye-dropper list-alt sliders
+
+
+ +

-
- angle-up +
+ + angle-up + + + angle-up +
-
+