From b56b4ee192962a6c5c679121fdfcb11a367cc5b4 Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Thu, 8 Feb 2024 11:42:20 +0800 Subject: [PATCH] rm cookie --- components/CustomContextMenu.js | 4 +- lib/global.js | 9 +- lib/lang.js | 13 +- package.json | 3 +- themes/heo/components/DarkModeButton.js | 4 +- themes/heo/components/FloatDarkModeButton.js | 4 +- themes/hexo/components/FloatDarkModeButton.js | 4 +- .../landing-2/components/BackToTopButton.js | 19 +- themes/landing-2/components/DarkModeButton.js | 83 ++++ themes/landing-2/components/Logo.js | 60 +++ themes/landing-2/components/MenuList.js | 183 ++++++++ themes/landing-2/components/NavBar.js | 411 +++--------------- themes/landing-2/style.js | 132 +++++- .../matery/components/FloatDarkModeButton.js | 4 +- themes/next/components/DarkModeButton.js | 4 +- themes/next/components/FloatDarkModeButton.js | 4 +- themes/theme.js | 29 +- yarn.lock | 13 - 18 files changed, 563 insertions(+), 420 deletions(-) create mode 100644 themes/landing-2/components/DarkModeButton.js create mode 100644 themes/landing-2/components/Logo.js create mode 100644 themes/landing-2/components/MenuList.js diff --git a/components/CustomContextMenu.js b/components/CustomContextMenu.js index ff0dc313..4398c331 100644 --- a/components/CustomContextMenu.js +++ b/components/CustomContextMenu.js @@ -2,7 +2,7 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState, useRef, useLayoutEffect } from 'react' import { useGlobal } from '@/lib/global' -import { saveDarkModeToCookies, THEMES } from '@/themes/theme' +import { saveDarkModeToLocalStorage, THEMES } from '@/themes/theme' import useWindowSize from '@/hooks/useWindowSize' import { siteConfig } from '@/lib/config' @@ -122,7 +122,7 @@ export default function CustomContextMenu(props) { function handleChangeDarkMode() { const newStatus = !isDarkMode - saveDarkModeToCookies(newStatus) + saveDarkModeToLocalStorage(newStatus) updateDarkMode(newStatus) const htmlElement = document.getElementsByTagName('html')[0] htmlElement.classList?.remove(newStatus ? 'light' : 'dark') diff --git a/lib/global.js b/lib/global.js index f1c84618..fc4cfa10 100644 --- a/lib/global.js +++ b/lib/global.js @@ -1,7 +1,7 @@ -import { generateLocaleDict, initLocale, saveLangToCookies } from './lang' +import { generateLocaleDict, initLocale, saveLangToLocalStorage } from './lang' import { createContext, useContext, useEffect, useState } from 'react' import { useRouter } from 'next/router' -import { THEMES, initDarkMode, saveDarkModeToCookies } from '@/themes/theme' +import { THEMES, initDarkMode, saveDarkModeToLocalStorage } from '@/themes/theme' import { APPEARANCE, LANG, THEME } from 'blog.config' const GlobalContext = createContext() @@ -38,7 +38,8 @@ export function GlobalContextProvider(props) { // 切换深色模式 const toggleDarkMode = () => { const newStatus = !isDarkMode - saveDarkModeToCookies(newStatus) + saveDarkModeToLocalStorage(newStatus) + console.log('切换中', isDarkMode, newStatus) updateDarkMode(newStatus) const htmlElement = document.getElementsByTagName('html')[0] htmlElement.classList?.remove(newStatus ? 'light' : 'dark') @@ -50,7 +51,7 @@ export function GlobalContextProvider(props) { */ function changeLang(lang) { if (lang) { - saveLangToCookies(lang) + saveLangToLocalStorage(lang) updateLang(lang) updateLocale(generateLocaleDict(lang)) } diff --git a/lib/lang.js b/lib/lang.js index 5f81af18..eb6a8890 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -5,7 +5,6 @@ import zhTW from './lang/zh-TW' import frFR from './lang/fr-FR' import trTR from './lang/tr-TR' import jaJP from './lang/ja-JP' -import cookie from 'react-cookies' import { getQueryVariable, isBrowser, mergeDeep } from './utils' /** @@ -65,14 +64,14 @@ export function generateLocaleDict(langString) { */ export function initLocale(lang, locale, changeLang, changeLocale) { if (isBrowser) { - const queryLang = getQueryVariable('lang') || loadLangFromCookies() + const queryLang = getQueryVariable('lang') || loadLangFromLocalStorage() let currentLang = lang if (queryLang && queryLang !== 'undefined' && queryLang !== lang) { currentLang = queryLang } changeLang(currentLang) - saveLangToCookies(currentLang) + saveLangToLocalStorage(currentLang) const targetLocale = generateLocaleDict(currentLang) if (JSON.stringify(locale) !== JSON.stringify(currentLang)) { @@ -84,14 +83,14 @@ export function initLocale(lang, locale, changeLang, changeLocale) { * 读取语言 * @returns {*} */ -export const loadLangFromCookies = () => { - return cookie.load('lang') +export const loadLangFromLocalStorage = () => { + return localStorage.getItem('lang') } /** * 保存语言 * @param newTheme */ -export const saveLangToCookies = (lang) => { - cookie.save('lang', lang, { path: '/' }) +export const saveLangToLocalStorage = (lang) => { + localStorage.setItem('lang', lang) } diff --git a/package.json b/package.json index 82efcf06..333f945f 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "preact": "^10.5.15", "prism-themes": "1.9.0", "react": "^18.2.0", - "react-cookies": "^0.1.1", "react-dom": "^18.2.0", "react-facebook": "^8.1.4", "react-notion-x": "6.16.0", @@ -75,4 +74,4 @@ "url": "https://github.com/tangly/NotionNext/issues", "email": "tlyong1992@hotmail.com" } -} \ No newline at end of file +} diff --git a/themes/heo/components/DarkModeButton.js b/themes/heo/components/DarkModeButton.js index 9b56e6da..bef62962 100644 --- a/themes/heo/components/DarkModeButton.js +++ b/themes/heo/components/DarkModeButton.js @@ -1,5 +1,5 @@ import { useGlobal } from '@/lib/global' -import { saveDarkModeToCookies } from '@/themes/theme' +import { saveDarkModeToLocalStorage } from '@/themes/theme' import { Moon, Sun } from '@/components/HeroIcons' import { useImperativeHandle } from 'react' @@ -24,7 +24,7 @@ const DarkModeButton = (props) => { // 用户手动设置主题 const handleChangeDarkMode = () => { const newStatus = !isDarkMode - saveDarkModeToCookies(newStatus) + saveDarkModeToLocalStorage(newStatus) updateDarkMode(newStatus) const htmlElement = document.getElementsByTagName('html')[0] htmlElement.classList?.remove(newStatus ? 'light' : 'dark') diff --git a/themes/heo/components/FloatDarkModeButton.js b/themes/heo/components/FloatDarkModeButton.js index ec480ee4..0918093b 100644 --- a/themes/heo/components/FloatDarkModeButton.js +++ b/themes/heo/components/FloatDarkModeButton.js @@ -1,5 +1,5 @@ import { useGlobal } from '@/lib/global' -import { saveDarkModeToCookies } from '@/themes/theme' +import { saveDarkModeToLocalStorage } from '@/themes/theme' import CONFIG from '../config' import { siteConfig } from '@/lib/config' @@ -13,7 +13,7 @@ export default function FloatDarkModeButton () { // 用户手动设置主题 const handleChangeDarkMode = () => { const newStatus = !isDarkMode - saveDarkModeToCookies(newStatus) + saveDarkModeToLocalStorage(newStatus) updateDarkMode(newStatus) const htmlElement = document.getElementsByTagName('html')[0] htmlElement.classList?.remove(newStatus ? 'light' : 'dark') diff --git a/themes/hexo/components/FloatDarkModeButton.js b/themes/hexo/components/FloatDarkModeButton.js index d5848b06..6a7fb97e 100644 --- a/themes/hexo/components/FloatDarkModeButton.js +++ b/themes/hexo/components/FloatDarkModeButton.js @@ -1,5 +1,5 @@ import { useGlobal } from '@/lib/global' -import { saveDarkModeToCookies } from '@/themes/theme' +import { saveDarkModeToLocalStorage } from '@/themes/theme' import CONFIG from '../config' import { siteConfig } from '@/lib/config' @@ -13,7 +13,7 @@ export default function FloatDarkModeButton () { // 用户手动设置主题 const handleChangeDarkMode = () => { const newStatus = !isDarkMode - saveDarkModeToCookies(newStatus) + saveDarkModeToLocalStorage(newStatus) updateDarkMode(newStatus) const htmlElement = document.getElementsByTagName('html')[0] htmlElement.classList?.remove(newStatus ? 'light' : 'dark') diff --git a/themes/landing-2/components/BackToTopButton.js b/themes/landing-2/components/BackToTopButton.js index 799de8f6..bcb404de 100644 --- a/themes/landing-2/components/BackToTopButton.js +++ b/themes/landing-2/components/BackToTopButton.js @@ -1,4 +1,5 @@ -import { useEffect } from 'react' +import throttle from 'lodash.throttle'; +import { useCallback, useEffect } from 'react' export const BackToTopButton = () => { useEffect(() => { @@ -34,8 +35,24 @@ export const BackToTopButton = () => { document.querySelector('.back-to-top').onclick = () => { scrollTo(document.documentElement); }; + + window.addEventListener('scroll', navBarScollListener) + return () => { + window.removeEventListener('scroll', navBarScollListener) + } }) + // 滚动监听 + const throttleMs = 200 + const navBarScollListener = useCallback( + throttle(() => { + const scrollY = window.scrollY; + // 显示或隐藏返回顶部按钮 + const backToTop = document.querySelector('.back-to-top'); + backToTop.style.display = scrollY > 50 ? 'flex' : 'none'; + }, throttleMs) + ) + return <> {/* */} { + const { toggleDarkMode } = useGlobal() + + return <> + + +} diff --git a/themes/landing-2/components/Logo.js b/themes/landing-2/components/Logo.js new file mode 100644 index 00000000..06732b1c --- /dev/null +++ b/themes/landing-2/components/Logo.js @@ -0,0 +1,60 @@ +import throttle from 'lodash.throttle'; +import Link from 'next/link' +import { useCallback, useEffect } from 'react'; + +/** + * 站点图标 + * @returns + */ +export const Logo = () => { + useEffect(() => { + window.addEventListener('scroll', navBarScollListener) + return () => { + window.removeEventListener('scroll', navBarScollListener) + } + }, []) + + // 滚动监听 + const throttleMs = 200 + const navBarScollListener = useCallback( + throttle(() => { + // eslint-disable-next-line camelcase + const ud_header = document.querySelector('.ud-header'); + const logo = document.querySelector('.header-logo'); + + const scrollY = window.scrollY; + + // 控制台输出当前滚动位置和 sticky 值 + if (scrollY > 0) { + ud_header.classList.add('sticky'); + // 根据导航栏状态修改 logo + if (logo) { + logo.src = '/images/landing-2/logo/logo.svg'; + } + } else { + ud_header.classList.remove('sticky'); + // 根据导航栏状态修改 logo + if (logo) { + logo.src = '/images/landing-2/logo/logo-white.svg'; + } + } + + // 显示或隐藏返回顶部按钮 + const backToTop = document.querySelector('.back-to-top'); + backToTop.style.display = scrollY > 50 ? 'flex' : 'none'; + }, throttleMs) + ) + + return <> +
+ + {/* eslint-disable-next-line @next/next/no-img-element */} + logo + +
+ +} diff --git a/themes/landing-2/components/MenuList.js b/themes/landing-2/components/MenuList.js new file mode 100644 index 00000000..c64526cd --- /dev/null +++ b/themes/landing-2/components/MenuList.js @@ -0,0 +1,183 @@ +import { useEffect } from 'react'; +/** + * 响应式 折叠菜单 + */ +export const MenuList = () => { + useEffect(() => { + // ===== responsive navbar + const navbarToggler = document.querySelector('#navbarToggler'); + const navbarCollapse = document.querySelector('#navbarCollapse'); + + navbarToggler.addEventListener('click', () => { + navbarToggler.classList.toggle('navbarTogglerActive'); + navbarCollapse.classList.toggle('hidden'); + }); + + //= ==== close navbar-collapse when a clicked + document + .querySelectorAll('#navbarCollapse ul li:not(.submenu-item) a') + .forEach((e) => + e.addEventListener('click', () => { + navbarToggler.classList.remove('navbarTogglerActive'); + navbarCollapse.classList.add('hidden'); + }) + ); + + // ===== Sub-menu + const submenuItems = document.querySelectorAll('.submenu-item'); + submenuItems.forEach((el) => { + el.querySelector('a').addEventListener('click', () => { + el.querySelector('.submenu').classList.toggle('hidden'); + }); + }); + }, []) + + return <> + +
+ + +
+ + +} diff --git a/themes/landing-2/components/NavBar.js b/themes/landing-2/components/NavBar.js index 4f9f70af..e0b6c739 100644 --- a/themes/landing-2/components/NavBar.js +++ b/themes/landing-2/components/NavBar.js @@ -1,373 +1,78 @@ /* eslint-disable no-unreachable */ -import { useEffect } from 'react' +import throttle from 'lodash.throttle'; +import { useCallback, useEffect } from 'react' +import { MenuList } from './MenuList'; +import { DarkModeButton } from './DarkModeButton'; +import { Logo } from './Logo'; +/** + * 顶部导航栏 + */ export const NavBar = () => { useEffect(() => { // ======= Sticky - window.onscroll = function () { - // 这里有点bug - return - // eslint-disable-next-line camelcase - const ud_header = document.querySelector('.ud-header'); - const sticky = ud_header.offsetTop; - const logo = document.querySelectorAll('.header-logo'); + window.addEventListener('scroll', navBarScollListener) + return () => { + window.removeEventListener('scroll', navBarScollListener) + } + }, []) - if (window.pageYOffset > sticky) { + // 滚动监听 + const throttleMs = 200 + const navBarScollListener = useCallback( + throttle(() => { + // eslint-disable-next-line camelcase + const ud_header = document.querySelector('.ud-header'); + const scrollY = window.scrollY; + // 控制台输出当前滚动位置和 sticky 值 + if (scrollY > 0) { ud_header.classList.add('sticky'); } else { ud_header.classList.remove('sticky'); } - - if (logo.length) { - // === logo change - if (ud_header.classList.contains('sticky')) { - document.querySelector('.header-logo').src = - '/images/landing-2/logo/logo.svg' - } else { - document.querySelector('.header-logo').src = - '/images/landing-2/logo/logo-white.svg' - } - } - - if (document.documentElement.classList.contains('dark')) { - if (logo.length) { - // === logo change - if (ud_header.classList.contains('sticky')) { - document.querySelector('.header-logo').src = - '/images/landing-2/logo/logo-white.svg' - } - } - } - - // show or hide the back-top-top button - const backToTop = document.querySelector('.back-to-top'); - if ( - document.body.scrollTop > 50 || - document.documentElement.scrollTop > 50 - ) { - backToTop.style.display = 'flex'; - } else { - backToTop.style.display = 'none'; - } - }; - - // ===== responsive navbar - const navbarToggler = document.querySelector('#navbarToggler'); - const navbarCollapse = document.querySelector('#navbarCollapse'); - - navbarToggler.addEventListener('click', () => { - navbarToggler.classList.toggle('navbarTogglerActive'); - navbarCollapse.classList.toggle('hidden'); - }); - - //= ==== close navbar-collapse when a clicked - document - .querySelectorAll('#navbarCollapse ul li:not(.submenu-item) a') - .forEach((e) => - e.addEventListener('click', () => { - navbarToggler.classList.remove('navbarTogglerActive'); - navbarCollapse.classList.add('hidden'); - }) - ); - - // ===== Sub-menu - const submenuItems = document.querySelectorAll('.submenu-item'); - submenuItems.forEach((el) => { - el.querySelector('a').addEventListener('click', () => { - el.querySelector('.submenu').classList.toggle('hidden'); - }); - }); - - /* ======== themeSwitcher start ========= */ - - // themeSwitcher - const themeSwitcher = document.getElementById('themeSwitcher'); - - // Theme Vars - const userTheme = localStorage.getItem('theme'); - const systemTheme = window.matchMedia('(prefers-color0scheme: dark)').matches; - - // Initial Theme Check - const themeCheck = () => { - if (userTheme === 'dark' || (!userTheme && systemTheme)) { - document.documentElement.classList.add('dark'); - } - }; - - // Manual Theme Switch - const themeSwitch = () => { - if (document.documentElement.classList.contains('dark')) { - document.documentElement.classList.remove('dark'); - localStorage.setItem('theme', 'light'); - return; - } - - document.documentElement.classList.add('dark'); - localStorage.setItem('theme', 'dark'); - }; - - // call theme switch on clicking buttons - themeSwitcher.addEventListener('click', () => { - themeSwitch(); - }); - - // invoke theme check on initial load - themeCheck(); - /* ======== themeSwitcher End ========= */ - }) + }, throttleMs) + ) return <> {/* */} -
-
-
-
- - logo - -
-
-
- - + +
-
- - -
-
-
{/* */} diff --git a/themes/landing-2/style.js b/themes/landing-2/style.js index 19004e1d..b32ba726 100644 --- a/themes/landing-2/style.js +++ b/themes/landing-2/style.js @@ -8,9 +8,135 @@ const Style = () => { return } diff --git a/themes/matery/components/FloatDarkModeButton.js b/themes/matery/components/FloatDarkModeButton.js index e42dac8d..5db024c1 100644 --- a/themes/matery/components/FloatDarkModeButton.js +++ b/themes/matery/components/FloatDarkModeButton.js @@ -1,5 +1,5 @@ import { useGlobal } from '@/lib/global' -import { saveDarkModeToCookies } from '@/themes/theme' +import { saveDarkModeToLocalStorage } from '@/themes/theme' import CONFIG from '../config' import { siteConfig } from '@/lib/config' @@ -13,7 +13,7 @@ export default function FloatDarkModeButton() { // 用户手动设置主题 const handleChangeDarkMode = () => { const newStatus = !isDarkMode - saveDarkModeToCookies(newStatus) + saveDarkModeToLocalStorage(newStatus) updateDarkMode(newStatus) const htmlElement = document.getElementsByTagName('html')[0] htmlElement.classList?.remove(newStatus ? 'light' : 'dark') diff --git a/themes/next/components/DarkModeButton.js b/themes/next/components/DarkModeButton.js index a1b5aea5..8858f6b7 100644 --- a/themes/next/components/DarkModeButton.js +++ b/themes/next/components/DarkModeButton.js @@ -1,12 +1,12 @@ import { useGlobal } from '@/lib/global' -import { saveDarkModeToCookies } from '@/themes/theme' +import { saveDarkModeToLocalStorage } from '@/themes/theme' const DarkModeButton = () => { const { isDarkMode, updateDarkMode } = useGlobal() // 用户手动设置主题 const handleChangeDarkMode = () => { const newStatus = !isDarkMode - saveDarkModeToCookies(newStatus) + saveDarkModeToLocalStorage(newStatus) updateDarkMode(newStatus) const htmlElement = document.getElementsByTagName('html')[0] htmlElement.classList?.remove(newStatus ? 'light' : 'dark') diff --git a/themes/next/components/FloatDarkModeButton.js b/themes/next/components/FloatDarkModeButton.js index 0cc5e5bf..3412f646 100644 --- a/themes/next/components/FloatDarkModeButton.js +++ b/themes/next/components/FloatDarkModeButton.js @@ -1,5 +1,5 @@ import { useGlobal } from '@/lib/global' -import { saveDarkModeToCookies } from '@/themes/theme' +import { saveDarkModeToLocalStorage } from '@/themes/theme' import CONFIG from '../config' import { siteConfig } from '@/lib/config' @@ -13,7 +13,7 @@ export default function FloatDarkModeButton () { // 用户手动设置主题 const handleChangeDarkMode = () => { const newStatus = !isDarkMode - saveDarkModeToCookies(newStatus) + saveDarkModeToLocalStorage(newStatus) updateDarkMode(newStatus) const htmlElement = document.getElementsByTagName('html')[0] htmlElement.classList?.remove(newStatus ? 'light' : 'dark') diff --git a/themes/theme.js b/themes/theme.js index da9e1b66..599621e2 100644 --- a/themes/theme.js +++ b/themes/theme.js @@ -1,4 +1,3 @@ -import cookie from 'react-cookies' import BLOG from '@/blog.config' import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils' import dynamic from 'next/dynamic' @@ -108,7 +107,7 @@ export const initDarkMode = (updateDarkMode) => { let newDarkMode = isPreferDark() // 查看cookie中是否用户强制设置深色模式 - const cookieDarkMode = loadDarkModeFromCookies() + const cookieDarkMode = loadDarkModeFromLocalStorage() if (cookieDarkMode) { newDarkMode = JSON.parse(cookieDarkMode) } @@ -120,7 +119,7 @@ export const initDarkMode = (updateDarkMode) => { } updateDarkMode(newDarkMode) - saveDarkModeToCookies(newDarkMode) + saveDarkModeToLocalStorage(newDarkMode) document.getElementsByTagName('html')[0].setAttribute('class', newDarkMode ? 'dark' : 'light') } @@ -145,30 +144,14 @@ export function isPreferDark() { * 读取深色模式 * @returns {*} */ -export const loadDarkModeFromCookies = () => { - return cookie.load('darkMode') +export const loadDarkModeFromLocalStorage = () => { + return localStorage.getItem('darkMode') } /** * 保存深色模式 * @param newTheme */ -export const saveDarkModeToCookies = (newTheme) => { - cookie.save('darkMode', newTheme, { path: '/' }) -} - -/** - * 读取默认主题 - * @returns {*} - */ -export const loadThemeFromCookies = () => { - return cookie.load('theme') -} - -/** - * 保存默认主题 - * @param newTheme - */ -export const saveThemeToCookies = (newTheme) => { - cookie.save('theme', newTheme, { path: '/' }) +export const saveDarkModeToLocalStorage = (newTheme) => { + localStorage.setItem('darkMode', newTheme) } diff --git a/yarn.lock b/yarn.lock index 1d1a0511..bf13ffd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2070,11 +2070,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -cookie@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - integrity sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw== - copy-to-clipboard@^3.3.1: version "3.3.3" resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" @@ -4188,14 +4183,6 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -react-cookies@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/react-cookies/-/react-cookies-0.1.1.tgz#2a35807e04f5a13f58ccd1a9fb66574506873c88" - integrity sha512-PP75kJ4vtoHuuTdq0TAD3RmlAv7vuDQh9fkC4oDlhntgs9vX1DmREomO0Y1mcQKR9nMZ6/zxoflaMJ3MAmF5KQ== - dependencies: - cookie "^0.3.1" - object-assign "^4.1.1" - react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"