fix: fix menu is not working after OpenAI page update

This commit is contained in:
Pionxzh
2023-04-05 20:18:22 +08:00
parent d48a01552b
commit 9632818845
9 changed files with 284 additions and 176 deletions

View File

@@ -22,6 +22,7 @@
"dependencies": {
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-hover-card": "^1.0.5",
"eventemitter3": "^5.0.0",
"hast-util-to-html": "^8.0.4",
"html2canvas": "^1.4.1",

View File

@@ -27,7 +27,7 @@ main()
function main() {
onloadSafe(() => {
const container = document.createElement('div')
render(<Menu />, container)
render(<Menu container={container} />, container)
sentinel.on('nav', (nav) => {
const chatList = document.querySelector('nav > div.overflow-y-auto')

View File

@@ -25,7 +25,6 @@ p > img[src*="https://images.unsplash.com/"] {
}
.menu-item[disabled] {
pointer-events: none;
filter: brightness(0.5);
}
@@ -53,101 +52,17 @@ p > img[src*="https://images.unsplash.com/"] {
}
.dropdown-backdrop {
display: none;
display: block;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0,0,0,.5);
z-index: 998;
animation-name: pointerFadeIn;
animation-duration: .3s;
}
.dropdown-menu {
display: none;
position: absolute;
grid-template-columns: 1fr 1fr;
column-gap: 4px;
left: calc(100% + 1rem);
top: -3.85rem;
width: 268px;
padding: .75rem .4rem 0 .4rem;
border-radius: .375rem;
box-shadow: 0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);
animation: fadeIn .3s;
}
.dropdown-menu::before {
content: '';
position: absolute;
top: 0;
left: -1rem;
width: 1rem;
height: 100%;
}
#exporter-menu[disabled] {
cursor: not-allowed;
}
#exporter-menu:not([disabled]):hover::after {
content: '';
position: absolute;
top: 1.2rem;
right: -1rem;
width: 0;
height: 0;
border-top: .5rem solid transparent;
border-bottom: .5rem solid transparent;
border-right: .5rem solid #202123;
animation: fadeIn .3s;
}
@supports not selector(:has(.test:hover)) {
#exporter-menu:not([disabled]):hover .dropdown-menu {
display: grid;
}
}
@supports selector(:has(.test:hover)) {
#exporter-menu:not([disabled]):not(:has(.dropdown-backdrop:hover)):hover .dropdown-menu {
display: grid;
}
}
@media screen and (hover: none) and (max-width: 768px) {
.dropdown-menu {
position: fixed;
left: 0;
top: unset;
bottom: 0;
width: 100%;
border-radius: 0;
box-shadow: none;
z-index: 999;
animation: slideUp .3s;
}
@supports selector(:has(.test:hover)) {
#exporter-menu:not([disabled]):not(:has(.dropdown-backdrop:hover)):hover .dropdown-backdrop {
display: block;
}
#exporter-menu:not([disabled]):has(.dropdown-backdrop:hover):hover .dropdown-backdrop {
display: block;
pointer-events: none;
opacity: 0;
animation-name: pointerFadeInReverse;
}
}
#exporter-menu:hover::after {
display: none;
}
}
@keyframes fadeIn {
from {
opacity: 0;
@@ -177,18 +92,6 @@ p > img[src*="https://images.unsplash.com/"] {
}
}
/* Using `animation-direction: reverse` is not working in our case */
@keyframes pointerFadeInReverse {
from {
opacity: 1;
pointer-events: auto;
}
to {
opacity: 0;
pointer-events: none;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);

View File

@@ -1,3 +1,11 @@
.animate-fadeIn {
animation: fadeIn .3s;
}
.animate-slideUp {
animation: slideUp .3s;
}
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235/var(--tw-bg-opacity));
@@ -25,6 +33,14 @@
background-color: rgb(55 65 81/var(--tw-bg-opacity));
}
.fill-current {
fill: currentColor;
}
.gap-x-1 {
column-gap: 0.25rem;
}
.h-2\.5 {
height: 0.625rem;
}
@@ -33,6 +49,10 @@
margin-right: 2rem;
}
.pb-0 {
padding-bottom: 0;
}
.pr-8 {
padding-right: 2rem;
}

View File

@@ -1,12 +0,0 @@
import type { FC } from '../type'
export const Dropdown: FC = ({ children }) => {
return (
<>
<div className="dropdown-backdrop"></div>
<div className="dropdown-menu bg-gray-900">
{children}
</div>
</>
)
}

View File

@@ -1,4 +1,5 @@
import { useCallback, useMemo } from 'preact/hooks'
import * as HoverCard from '@radix-ui/react-hover-card'
import { useCallback, useMemo, useState } from 'preact/hooks'
import { exportToHtml } from '../exporter/html'
import { exportToPng } from '../exporter/image'
import { exportToJson } from '../exporter/json'
@@ -6,7 +7,6 @@ import { exportToMarkdown } from '../exporter/markdown'
import { exportToText } from '../exporter/text'
import { getHistoryDisabled } from '../page'
import { Divider } from './Divider'
import { Dropdown } from './Dropdown'
import { ExportDialog } from './ExportDialog'
import { FormatProvider, useFormatContext } from './FormatContext'
import { FileCode, IconArrowRightFromBracket, IconCamera, IconCopy, IconJSON, IconMarkdown, IconSetting, IconZip } from './Icons'
@@ -17,14 +17,15 @@ import { SettingDialog } from './SettingDialog'
import '../style.css'
import './Dialog.css'
function MenuInner() {
const disabled = getHistoryDisabled()
const menuText = disabled ? 'Exporter unavailable' : 'Export'
const menuTitle = disabled
? `Exporter is relying on the History API.
const disabledTitle = `Exporter is relying on the History API.
But History feature is disabled by OpenAI temporarily.
We all have to wait for them to bring it back.`
: ''
function MenuInner({ container }: { container: HTMLDivElement }) {
const disabled = getHistoryDisabled()
const [open, setOpen] = useState(false)
const [dialogOpen, setDialogOpen] = useState(false)
const { format } = useFormatContext()
const { enableMeta, exportMetaList } = useMetaDataContext()
@@ -36,70 +37,129 @@ We all have to wait for them to bring it back.`
const onClickHtml = useCallback(() => exportToHtml(format, metaList), [format, metaList])
const onClickJSON = useCallback(() => exportToJson(format), [format])
return (
<div id="exporter-menu" className="pt-1 relative" disabled={disabled} title={menuTitle}>
<MenuItem
text={menuText}
icon={IconArrowRightFromBracket}
disabled={disabled}
/>
<Dropdown>
<SettingDialog>
<div className="row-full">
<MenuItem text="Setting" icon={IconSetting} />
</div>
</SettingDialog>
const isMobile = window.innerWidth < 768
const Portal = isMobile ? 'div' : HoverCard.Portal
if (disabled) {
return (
<MenuItem
className="mt-1"
text="Exporter unavailable"
icon={IconArrowRightFromBracket}
title={disabledTitle}
disabled={true}
/>
)
}
return (
<>
{isMobile && open && (
<div
className="dropdown-backdrop animate-fadeIn"
onClick={() => setOpen(false)}
></div>
)}
<HoverCard.Root
openDelay={0}
closeDelay={200}
open={open}
onOpenChange={setOpen}
>
<HoverCard.Trigger>
<MenuItem
className="mt-1"
text="Export"
icon={IconArrowRightFromBracket}
onClick={() => {
console.log('click')
setOpen(true)
return true
}}
/>
</HoverCard.Trigger>
<Portal
container={isMobile ? container : document.body}
forceMount={open || dialogOpen}
>
<HoverCard.Content
className={isMobile
? 'fixed grid grid-cols-2 gap-x-1 px-1.5 py-2 bg-gray-900 shadow-md transition-opacity duration-200 animate-slideUp'
: 'grid grid-cols-2 gap-x-1 px-1.5 py-2 pb-0 rounded-md bg-gray-900 shadow-md transition-opacity duration-200 animate-fadeIn'}
style={{
width: isMobile ? 316 : 268,
left: -6,
bottom: 'calc(-1 * var(--radix-popper-available-height))',
}}
sideOffset={8}
side={isMobile ? 'bottom' : 'right'}
align="start"
alignOffset={isMobile ? 0 : -64}
>
<SettingDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
>
<div className="row-full">
<MenuItem text="Setting" icon={IconSetting} />
</div>
</SettingDialog>
<MenuItem
text="Copy Text"
successText="Copied!"
icon={() => <IconCopy className="w-4 h-4" />}
className="row-full"
onClick={onClickText}
/>
<MenuItem
text="Screenshot"
icon={IconCamera}
className="row-half"
onClick={onClickPng}
/>
<MenuItem
text="Markdown"
icon={IconMarkdown}
className="row-half"
onClick={onClickMarkdown}
/>
<MenuItem
text="HTML"
icon={FileCode}
className="row-half"
onClick={onClickHtml}
/>
<MenuItem
text="JSON"
icon={IconJSON}
className="row-half"
onClick={onClickJSON}
/>
<ExportDialog format={format}>
<div className="row-full">
<MenuItem
text="Export All"
icon={IconZip}
text="Copy Text"
successText="Copied!"
icon={() => <IconCopy className="w-4 h-4" />}
className="row-full"
onClick={onClickText}
/>
</div>
</ExportDialog>
</Dropdown>
<MenuItem
text="Screenshot"
icon={IconCamera}
className="row-half"
onClick={onClickPng}
/>
<MenuItem
text="Markdown"
icon={IconMarkdown}
className="row-half"
onClick={onClickMarkdown}
/>
<MenuItem
text="HTML"
icon={FileCode}
className="row-half"
onClick={onClickHtml}
/>
<MenuItem
text="JSON"
icon={IconJSON}
className="row-half"
onClick={onClickJSON}
/>
<ExportDialog format={format}>
<div className="row-full">
<MenuItem
text="Export All"
icon={IconZip}
/>
</div>
</ExportDialog>
<HoverCard.Arrow width="16" height="8" className="text-gray-900 fill-current" />
</HoverCard.Content>
</Portal>
</HoverCard.Root>
<Divider />
</div>
</>
)
}
export function Menu() {
export function Menu({ container }: { container: HTMLDivElement }) {
return (
<FormatProvider>
<MetaDataProvider>
<MenuInner />
<MenuInner container={container} />
</MetaDataProvider>
</FormatProvider>
)

View File

@@ -9,11 +9,12 @@ export interface MenuItemProps {
icon?: FC
successText?: string
disabled?: boolean
title?: string
className?: string
onClick?: (() => boolean) | (() => Promise<boolean>)
}
export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = false, icon: Icon, onClick, className }) => {
export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = false, title, icon: Icon, onClick, className }) => {
const [loading, setLoading] = useState(false)
const [succeed, setSucceed] = useState(false)
@@ -40,7 +41,9 @@ export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = fals
<div
className={`menu-item flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm mb-2 flex-shrink-0 border border-white/20 ${className}`}
onClick={handleClick}
onTouchStart={handleClick}
disabled={disabled}
title={title}
>
{loading
? (

View File

@@ -15,7 +15,16 @@ const Variable = ({ name, title }: { name: string; title: string }) => (
<strong className="cursor-help select-all whitespace-nowrap" title={title}>{name}</strong>
)
export const SettingDialog: FC = ({ children }) => {
interface SettingDialogProps {
open: boolean
onOpenChange: (value: boolean) => void
}
export const SettingDialog: FC<SettingDialogProps> = ({
open,
onOpenChange,
children,
}) => {
const { format, setFormat } = useFormatContext()
const { enableMeta, setEnableMeta, exportMetaList, setExportMetaList } = useMetaDataContext()
@@ -29,7 +38,10 @@ export const SettingDialog: FC = ({ children }) => {
const source = `${baseUrl}/${chatId}`
return (
<Dialog.Root>
<Dialog.Root
open={open}
onOpenChange={onOpenChange}
>
<Dialog.Trigger asChild>
{children}
</Dialog.Trigger>

121
pnpm-lock.yaml generated
View File

@@ -38,6 +38,7 @@ importers:
'@preact/preset-vite': ^2.5.0
'@radix-ui/colors': ^0.1.8
'@radix-ui/react-dialog': ^1.0.3
'@radix-ui/react-hover-card': ^1.0.5
'@types/mdast': ^3.0.11
'@types/unist': ^2.0.6
eventemitter3: ^5.0.0
@@ -60,6 +61,7 @@ importers:
dependencies:
'@radix-ui/colors': 0.1.8
'@radix-ui/react-dialog': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-hover-card': 1.0.5_biqbaboplfbrettd7655fr4n2y
eventemitter3: 5.0.0
hast-util-to-html: 8.0.4
html2canvas: 1.4.1
@@ -727,6 +729,30 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@floating-ui/core/0.7.3:
resolution: {integrity: sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==}
dev: false
/@floating-ui/dom/0.5.4:
resolution: {integrity: sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==}
dependencies:
'@floating-ui/core': 0.7.3
dev: false
/@floating-ui/react-dom/0.7.2_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/dom': 0.5.4
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
use-isomorphic-layout-effect: 1.1.2_react@18.2.0
transitivePeerDependencies:
- '@types/react'
dev: false
/@humanwhocodes/config-array/0.11.8:
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
engines: {node: '>=10.10.0'}
@@ -934,6 +960,18 @@ packages:
'@babel/runtime': 7.21.0
dev: false
/@radix-ui/react-arrow/1.0.2_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-compose-refs/1.0.0_react@18.2.0:
resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==}
peerDependencies:
@@ -1018,6 +1056,28 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-hover-card/1.0.5_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-jXRuZEkxSWdHZbVyL0J46cm7pQjmOMpwJEFKY+VqAJnV+FxS+zIZExI1OCeIiDwCBzUy6If1FfouOsfqBxr86g==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0_react@18.2.0
'@radix-ui/react-context': 1.0.0_react@18.2.0
'@radix-ui/react-dismissable-layer': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-popper': 1.1.1_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-portal': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-presence': 1.0.0_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-use-controllable-state': 1.0.0_react@18.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-id/1.0.0_react@18.2.0:
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
peerDependencies:
@@ -1028,6 +1088,29 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-popper/1.1.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@floating-ui/react-dom': 0.7.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-arrow': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-compose-refs': 1.0.0_react@18.2.0
'@radix-ui/react-context': 1.0.0_react@18.2.0
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-use-callback-ref': 1.0.0_react@18.2.0
'@radix-ui/react-use-layout-effect': 1.0.0_react@18.2.0
'@radix-ui/react-use-rect': 1.0.0_react@18.2.0
'@radix-ui/react-use-size': 1.0.0_react@18.2.0
'@radix-ui/rect': 1.0.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-portal/1.0.2_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==}
peerDependencies:
@@ -1113,6 +1196,32 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-rect/1.0.0_react@18.2.0:
resolution: {integrity: sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/rect': 1.0.0
react: 18.2.0
dev: false
/@radix-ui/react-use-size/1.0.0_react@18.2.0:
resolution: {integrity: sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/react-use-layout-effect': 1.0.0_react@18.2.0
react: 18.2.0
dev: false
/@radix-ui/rect/1.0.0:
resolution: {integrity: sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==}
dependencies:
'@babel/runtime': 7.21.0
dev: false
/@rollup/pluginutils/4.2.1:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'}
@@ -5453,6 +5562,18 @@ packages:
tslib: 2.4.0
dev: false
/use-isomorphic-layout-effect/1.1.2_react@18.2.0:
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
react: 18.2.0
dev: false
/use-sidecar/1.1.2_react@18.2.0:
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}