mirror of
https://github.com/mengxi-ream/read-frog.git
synced 2026-04-30 01:56:46 +00:00
Compare commits
3 commits
main
...
feat/float
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
120dc01f4f | ||
|
|
ed5319949f | ||
|
|
7778259f89 |
11 changed files with 312 additions and 72 deletions
5
.changeset/itchy-nights-push.md
Normal file
5
.changeset/itchy-nights-push.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@read-frog/extension": patch
|
||||
---
|
||||
|
||||
feat: add floating button controls
|
||||
|
|
@ -22,6 +22,7 @@ vi.mock("@/utils/atoms/config", () => ({
|
|||
position: 0.66,
|
||||
clickAction: "panel",
|
||||
disabledFloatingButtonPatterns: [],
|
||||
locked: false,
|
||||
}),
|
||||
sideContent: atom({ width: 360 }),
|
||||
},
|
||||
|
|
@ -38,13 +39,13 @@ vi.mock("../../../index", () => ({
|
|||
}))
|
||||
|
||||
vi.mock("../translate-button", () => ({
|
||||
default: ({ className }: { className?: string }) => (
|
||||
default: ({ className }: { className?: string, expanded?: boolean }) => (
|
||||
<div data-testid="translate-button" className={className} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("../components/hidden-button", () => ({
|
||||
default: ({ className, onClick }: { className?: string, onClick: () => void }) => (
|
||||
default: ({ className, onClick }: { className?: string, onClick: () => void, expanded?: boolean }) => (
|
||||
<button type="button" data-testid="hidden-button" className={className} onClick={onClick} />
|
||||
),
|
||||
}))
|
||||
|
|
@ -63,25 +64,86 @@ beforeAll(() => {
|
|||
vi.stubGlobal("ResizeObserver", ResizeObserverMock)
|
||||
})
|
||||
|
||||
describe("floatingButton close trigger", () => {
|
||||
it("keeps the close trigger in the layout with visibility classes instead of display:none", () => {
|
||||
describe("floatingButton controls", () => {
|
||||
it("shows the close trigger only after entering the main floating button", () => {
|
||||
render(<FloatingButton />)
|
||||
|
||||
const closeTrigger = screen.getByTitle("Close floating button")
|
||||
const closeTrigger = screen.getByRole("button", { name: "Close floating button" })
|
||||
const mainButton = screen.getByRole("img").parentElement
|
||||
|
||||
expect(closeTrigger).toHaveClass("-top-1")
|
||||
expect(closeTrigger).toHaveClass("left-0")
|
||||
expect(closeTrigger).toHaveClass("invisible")
|
||||
expect(closeTrigger).toHaveClass("group-hover:visible")
|
||||
expect(closeTrigger).not.toHaveClass("hidden")
|
||||
expect(closeTrigger).not.toHaveClass("group-hover:block")
|
||||
expect(closeTrigger).toHaveClass("pointer-events-none")
|
||||
expect(closeTrigger).toHaveClass("text-neutral-400")
|
||||
expect(closeTrigger).toHaveClass("hover:scale-110")
|
||||
expect(closeTrigger).toHaveClass("active:scale-90")
|
||||
expect(closeTrigger).toHaveClass("hover:text-neutral-600")
|
||||
expect(closeTrigger).toHaveClass("active:text-neutral-600")
|
||||
|
||||
fireEvent.mouseEnter(mainButton!)
|
||||
|
||||
expect(closeTrigger).toHaveClass("visible")
|
||||
expect(closeTrigger).toHaveClass("pointer-events-auto")
|
||||
expect(closeTrigger).toHaveClass("-left-6")
|
||||
})
|
||||
|
||||
it("renders a lock trigger at the lower-left corner and keeps controls expanded after entering the main button", () => {
|
||||
render(<FloatingButton />)
|
||||
|
||||
const lockTrigger = screen.getByRole("button", { name: "Lock floating button" })
|
||||
const mainButton = screen.getByRole("img").parentElement
|
||||
const floatingButtonContainer = mainButton?.parentElement?.parentElement
|
||||
|
||||
expect(lockTrigger).toHaveClass("left-0")
|
||||
expect(lockTrigger).toHaveClass("-bottom-1")
|
||||
expect(lockTrigger).toHaveClass("invisible")
|
||||
expect(lockTrigger).toHaveClass("pointer-events-none")
|
||||
expect(lockTrigger).toHaveClass("text-neutral-400")
|
||||
expect(lockTrigger).toHaveClass("hover:scale-110")
|
||||
expect(lockTrigger).toHaveClass("active:scale-90")
|
||||
expect(lockTrigger).toHaveClass("hover:text-neutral-600")
|
||||
expect(lockTrigger).toHaveClass("active:text-neutral-600")
|
||||
expect(mainButton).toHaveClass("translate-x-6")
|
||||
|
||||
fireEvent.mouseEnter(mainButton!)
|
||||
|
||||
expect(lockTrigger).toHaveClass("visible")
|
||||
expect(lockTrigger).toHaveClass("pointer-events-auto")
|
||||
expect(lockTrigger).toHaveClass("-left-6")
|
||||
expect(mainButton).toHaveClass("translate-x-0")
|
||||
|
||||
fireEvent.click(lockTrigger)
|
||||
|
||||
const unlockTrigger = screen.getByRole("button", { name: "Unlock floating button" })
|
||||
|
||||
expect(unlockTrigger).toHaveClass("text-neutral-400")
|
||||
expect(unlockTrigger).toHaveClass("-left-6")
|
||||
expect(mainButton).toHaveClass("translate-x-0")
|
||||
expect(mainButton).toHaveClass("opacity-100")
|
||||
expect(mainButton).not.toHaveClass("translate-x-6")
|
||||
|
||||
fireEvent.mouseLeave(floatingButtonContainer!)
|
||||
|
||||
expect(mainButton).toHaveClass("translate-x-0")
|
||||
expect(mainButton).toHaveClass("opacity-60")
|
||||
|
||||
fireEvent.mouseEnter(mainButton!)
|
||||
|
||||
expect(mainButton).toHaveClass("opacity-100")
|
||||
})
|
||||
|
||||
it("forces the close trigger visible while the dropdown is open", () => {
|
||||
render(<FloatingButton />)
|
||||
|
||||
const closeTrigger = screen.getByTitle("Close floating button")
|
||||
const closeTrigger = screen.getByRole("button", { name: "Close floating button" })
|
||||
const mainButton = screen.getByRole("img").parentElement
|
||||
|
||||
fireEvent.mouseEnter(mainButton!)
|
||||
fireEvent.click(closeTrigger)
|
||||
|
||||
expect(closeTrigger).toHaveClass("visible")
|
||||
expect(closeTrigger).toHaveClass("pointer-events-auto")
|
||||
expect(screen.getByText("options.floatingButtonAndToolbar.floatingButton.closeMenu.disableForSite")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,17 +5,20 @@ export default function HiddenButton({
|
|||
onClick,
|
||||
children,
|
||||
className,
|
||||
expanded = false,
|
||||
}: {
|
||||
icon: React.ReactNode
|
||||
onClick: () => void
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
expanded?: boolean
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"border-border mr-2 translate-x-12 cursor-pointer rounded-full border bg-white shadow-lg p-1.5 text-neutral-600 dark:text-neutral-400 transition-transform duration-300 group-hover:translate-x-0 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-800",
|
||||
"border-border mr-2 cursor-pointer rounded-full border bg-white shadow-lg p-1.5 text-neutral-600 dark:text-neutral-400 transition-transform duration-300 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-800",
|
||||
expanded ? "translate-x-0" : "translate-x-12",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { browser, i18n } from "#imports"
|
||||
import { IconSettings, IconX } from "@tabler/icons-react"
|
||||
import { IconLock, IconLockOpen, IconSettings, IconX } from "@tabler/icons-react"
|
||||
import { useAtom, useAtomValue } from "jotai"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import readFrogLogo from "@/assets/icons/read-frog.png?url&no-inline"
|
||||
|
|
@ -22,6 +22,15 @@ import HiddenButton from "./components/hidden-button"
|
|||
import TranslateButton from "./translate-button"
|
||||
|
||||
const readFrogLogoUrl = new URL(readFrogLogo, browser.runtime.getURL("/")).href
|
||||
const floatingButtonControlClassName = cn(
|
||||
"absolute invisible cursor-pointer pointer-events-none flex size-6 items-center justify-center",
|
||||
"text-neutral-400 transition-[color,left,transform] duration-300 hover:scale-110 hover:text-neutral-600 active:scale-90 active:text-neutral-600",
|
||||
"dark:text-neutral-600 dark:hover:text-neutral-400 dark:active:text-neutral-400",
|
||||
)
|
||||
const floatingButtonControlOffsetClassNames = {
|
||||
collapsed: "left-0",
|
||||
expanded: "-left-6",
|
||||
}
|
||||
|
||||
export default function FloatingButton() {
|
||||
const [floatingButton, setFloatingButton] = useAtom(
|
||||
|
|
@ -32,8 +41,12 @@ export default function FloatingButton() {
|
|||
const [isSideOpen, setIsSideOpen] = useAtom(isSideOpenAtom)
|
||||
const [isDraggingButton, setIsDraggingButton] = useAtom(isDraggingButtonAtom)
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||
const [isHitAreaExpanded, setIsHitAreaExpanded] = useState(false)
|
||||
const [dragPosition, setDragPosition] = useState<number | null>(null)
|
||||
const initialClientYRef = useRef<number | null>(null)
|
||||
const isFloatingButtonLocked = floatingButton.locked
|
||||
const isFloatingButtonExpanded = isHitAreaExpanded || isDraggingButton || isSideOpen || isDropdownOpen
|
||||
const isMainButtonAttached = isFloatingButtonLocked || isFloatingButtonExpanded
|
||||
|
||||
// 按钮拖动处理
|
||||
useEffect(() => {
|
||||
|
|
@ -125,80 +138,63 @@ export default function FloatingButton() {
|
|||
|
||||
const attachSideClassName = isDraggingButton || isSideOpen || isDropdownOpen ? "translate-x-0" : ""
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHitAreaExpanded(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!isDropdownOpen) {
|
||||
setIsHitAreaExpanded(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!floatingButton.enabled || floatingButton.disabledFloatingButtonPatterns.some(pattern => matchDomainPattern(window.location.href, pattern))) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group fixed z-2147483647 flex flex-col items-end gap-2 print:hidden"
|
||||
className={cn(
|
||||
"fixed z-2147483647 flex flex-col items-end gap-2 print:hidden",
|
||||
isFloatingButtonExpanded && "pl-6",
|
||||
)}
|
||||
style={{
|
||||
right: isSideOpen
|
||||
? `calc(${sideContent.width}px + var(--removed-body-scroll-bar-size, 0px))`
|
||||
: "var(--removed-body-scroll-bar-size, 0px)",
|
||||
top: `${(dragPosition ?? floatingButton.position) * 100}vh`,
|
||||
}}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<TranslateButton className={attachSideClassName} />
|
||||
<div
|
||||
className={cn(
|
||||
"border-border flex h-10 w-11 items-center rounded-l-full border border-r-0 bg-white opacity-60 shadow-lg group-hover:opacity-100 dark:bg-neutral-900",
|
||||
"translate-x-6 transition-transform duration-300 group-hover:translate-x-0",
|
||||
(isSideOpen || isDropdownOpen) && "opacity-100",
|
||||
isDraggingButton ? "cursor-move" : "cursor-pointer",
|
||||
attachSideClassName,
|
||||
)}
|
||||
onMouseDown={handleButtonDragStart}
|
||||
>
|
||||
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
title="Close floating button"
|
||||
className={cn(
|
||||
"border-border absolute -top-1 -left-1 invisible cursor-pointer rounded-full border bg-neutral-100 dark:bg-neutral-900",
|
||||
"group-hover:visible",
|
||||
isDropdownOpen && "visible",
|
||||
)}
|
||||
onMouseDown={e => e.stopPropagation()} // 父级不会收到 mousedown
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<IconX className="h-3 w-3 text-neutral-400 dark:text-neutral-600" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent container={shadowWrapper} align="start" side="left" className="z-2147483647 w-fit! whitespace-nowrap">
|
||||
<DropdownMenuItem
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={() => {
|
||||
const currentDomain = window.location.hostname
|
||||
const currentPatterns = floatingButton.disabledFloatingButtonPatterns || []
|
||||
void setFloatingButton({
|
||||
...floatingButton,
|
||||
disabledFloatingButtonPatterns: [...currentPatterns, currentDomain],
|
||||
})
|
||||
}}
|
||||
>
|
||||
{i18n.t("options.floatingButtonAndToolbar.floatingButton.closeMenu.disableForSite")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={() => {
|
||||
void setFloatingButton({ ...floatingButton, enabled: false })
|
||||
}}
|
||||
>
|
||||
{i18n.t("options.floatingButtonAndToolbar.floatingButton.closeMenu.disableGlobally")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<img
|
||||
src={readFrogLogoUrl}
|
||||
alt={APP_NAME}
|
||||
className="ml-1 h-8 w-8 rounded-full"
|
||||
<TranslateButton className={attachSideClassName} expanded={isFloatingButtonExpanded} />
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
"border-border relative flex h-10 w-11 items-center rounded-l-full border border-r-0 bg-white shadow-lg transition-transform duration-300 dark:bg-neutral-900",
|
||||
isMainButtonAttached ? "translate-x-0" : "translate-x-6",
|
||||
isFloatingButtonExpanded ? "opacity-100" : "opacity-60",
|
||||
isDraggingButton ? "cursor-move" : "cursor-pointer",
|
||||
attachSideClassName,
|
||||
)}
|
||||
onMouseDown={handleButtonDragStart}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
<img
|
||||
src={readFrogLogoUrl}
|
||||
alt={APP_NAME}
|
||||
className="ml-1 h-8 w-8 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FloatingButtonCloseMenu
|
||||
expanded={isFloatingButtonExpanded}
|
||||
onDropdownOpenChange={setIsDropdownOpen}
|
||||
/>
|
||||
<FloatingButtonLockControl expanded={isFloatingButtonExpanded} />
|
||||
</div>
|
||||
<HiddenButton
|
||||
className={attachSideClassName}
|
||||
expanded={isFloatingButtonExpanded}
|
||||
icon={<IconSettings className="h-5 w-5" />}
|
||||
onClick={() => {
|
||||
void sendMessage("openOptionsPage", undefined)
|
||||
|
|
@ -207,3 +203,108 @@ export default function FloatingButton() {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface FloatingButtonCloseMenuProps {
|
||||
expanded: boolean
|
||||
onDropdownOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
function FloatingButtonCloseMenu({
|
||||
expanded,
|
||||
onDropdownOpenChange,
|
||||
}: FloatingButtonCloseMenuProps) {
|
||||
const [floatingButton, setFloatingButton] = useAtom(configFieldsAtomMap.floatingButton)
|
||||
const [open, setOpen] = useState(false)
|
||||
const controlOffsetClassName = !floatingButton.locked && !expanded
|
||||
? floatingButtonControlOffsetClassNames.collapsed
|
||||
: floatingButtonControlOffsetClassNames.expanded
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
onDropdownOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
const handleDisableForSite = () => {
|
||||
const currentDomain = window.location.hostname
|
||||
const currentPatterns = floatingButton.disabledFloatingButtonPatterns || []
|
||||
|
||||
void setFloatingButton({
|
||||
...floatingButton,
|
||||
disabledFloatingButtonPatterns: [...currentPatterns, currentDomain],
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisableGlobally = () => {
|
||||
void setFloatingButton({ ...floatingButton, enabled: false })
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close floating button"
|
||||
className={cn(
|
||||
floatingButtonControlClassName,
|
||||
"-top-1",
|
||||
controlOffsetClassName,
|
||||
expanded && "visible pointer-events-auto",
|
||||
open && "visible pointer-events-auto",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<IconX className="h-3 w-3" strokeWidth={3} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent container={shadowWrapper} align="start" side="left" className="z-2147483647 w-fit! whitespace-nowrap">
|
||||
<DropdownMenuItem
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={handleDisableForSite}
|
||||
>
|
||||
{i18n.t("options.floatingButtonAndToolbar.floatingButton.closeMenu.disableForSite")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={handleDisableGlobally}
|
||||
>
|
||||
{i18n.t("options.floatingButtonAndToolbar.floatingButton.closeMenu.disableGlobally")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
interface FloatingButtonLockControlProps {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
function FloatingButtonLockControl({ expanded }: FloatingButtonLockControlProps) {
|
||||
const [floatingButton, setFloatingButton] = useAtom(configFieldsAtomMap.floatingButton)
|
||||
const locked = floatingButton.locked
|
||||
const controlOffsetClassName = !locked && !expanded
|
||||
? floatingButtonControlOffsetClassNames.collapsed
|
||||
: floatingButtonControlOffsetClassNames.expanded
|
||||
|
||||
const handleToggleLocked = () => {
|
||||
void setFloatingButton({ ...floatingButton, locked: !locked })
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={locked ? "Unlock floating button" : "Lock floating button"}
|
||||
className={cn(
|
||||
floatingButtonControlClassName,
|
||||
"-bottom-1",
|
||||
controlOffsetClassName,
|
||||
expanded && "visible pointer-events-auto",
|
||||
)}
|
||||
onClick={handleToggleLocked}
|
||||
>
|
||||
{locked
|
||||
? <IconLock className="h-3 w-3" strokeWidth={3} />
|
||||
: <IconLockOpen className="h-3 w-3" strokeWidth={3} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { cn } from "@/utils/styles/utils"
|
|||
import { enablePageTranslationAtom } from "../../atoms"
|
||||
import HiddenButton from "./components/hidden-button"
|
||||
|
||||
export default function TranslateButton({ className }: { className: string }) {
|
||||
export default function TranslateButton({ className, expanded = false }: { className: string, expanded?: boolean }) {
|
||||
const translationState = useAtomValue(enablePageTranslationAtom)
|
||||
const isEnabled = translationState.enabled
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ export default function TranslateButton({ className }: { className: string }) {
|
|||
<HiddenButton
|
||||
icon={<RiTranslate className="h-5 w-5" />}
|
||||
className={className}
|
||||
expanded={expanded}
|
||||
onClick={() => {
|
||||
void sendMessage("tryToSetEnablePageTranslationOnContentScript", { enabled: !isEnabled })
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ options:
|
|||
title: 启用悬浮按钮
|
||||
description: 在网页上显示悬浮按钮,快速访问陪读蛙功能
|
||||
closeMenu:
|
||||
disableForSite: 禁用此网站
|
||||
disableForSite: 当前网站禁用
|
||||
disableGlobally: 全局禁用
|
||||
disabledSites:
|
||||
title: 悬浮按钮禁用网站
|
||||
|
|
@ -300,7 +300,7 @@ options:
|
|||
translate: 翻译
|
||||
speak: 朗读
|
||||
closeMenu:
|
||||
disableForSite: 禁用此网站
|
||||
disableForSite: 当前网站禁用
|
||||
disableGlobally: 全局禁用
|
||||
disabledSites:
|
||||
title: 选择工具栏禁用网站
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const floatingButtonSchema = z.object({
|
|||
position: z.number().min(0).max(1),
|
||||
disabledFloatingButtonPatterns: z.array(z.string()),
|
||||
clickAction: z.enum(["panel", "translate"]),
|
||||
locked: z.boolean(),
|
||||
})
|
||||
|
||||
const selectionToolbarFeatureSchema = z.object({
|
||||
|
|
|
|||
18
src/utils/config/__tests__/example/v068.ts
Normal file
18
src/utils/config/__tests__/example/v068.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { VersionTestData } from "./types"
|
||||
import { testSeries as v067TestSeries } from "./v067"
|
||||
|
||||
export const testSeries = Object.fromEntries(
|
||||
Object.entries(v067TestSeries).map(([seriesId, seriesData]) => [
|
||||
seriesId,
|
||||
{
|
||||
...seriesData,
|
||||
config: {
|
||||
...seriesData.config,
|
||||
floatingButton: {
|
||||
...seriesData.config.floatingButton,
|
||||
locked: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
) as VersionTestData["testSeries"]
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { describe, expect, it } from "vitest"
|
||||
import { migrate } from "../../migration-scripts/v067-to-v068"
|
||||
|
||||
describe("v067-to-v068 migration", () => {
|
||||
it("adds floatingButton.locked with a default false value", () => {
|
||||
const migrated = migrate({
|
||||
floatingButton: {
|
||||
enabled: true,
|
||||
position: 0.66,
|
||||
disabledFloatingButtonPatterns: [],
|
||||
clickAction: "translate",
|
||||
},
|
||||
})
|
||||
|
||||
expect(migrated.floatingButton.locked).toBe(false)
|
||||
})
|
||||
|
||||
it("preserves an existing floatingButton.locked value", () => {
|
||||
const migrated = migrate({
|
||||
floatingButton: {
|
||||
enabled: true,
|
||||
position: 0.66,
|
||||
disabledFloatingButtonPatterns: [],
|
||||
clickAction: "translate",
|
||||
locked: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(migrated.floatingButton.locked).toBe(true)
|
||||
})
|
||||
})
|
||||
17
src/utils/config/migration-scripts/v067-to-v068.ts
Normal file
17
src/utils/config/migration-scripts/v067-to-v068.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Migration script from v067 to v068
|
||||
* - Adds `floatingButton.locked` with a default value of `false`
|
||||
*
|
||||
* IMPORTANT: All values are hardcoded inline. Migration scripts are frozen
|
||||
* snapshots — never import constants or helpers that may change.
|
||||
*/
|
||||
|
||||
export function migrate(oldConfig: any): any {
|
||||
return {
|
||||
...oldConfig,
|
||||
floatingButton: {
|
||||
...oldConfig?.floatingButton,
|
||||
locked: oldConfig?.floatingButton?.locked ?? false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ export const GOOGLE_DRIVE_TOKEN_STORAGE_KEY = "__googleDriveToken"
|
|||
export const THEME_STORAGE_KEY = "theme"
|
||||
export const DETECTED_CODE_STORAGE_KEY = "detectedCode"
|
||||
export const DEFAULT_DETECTED_CODE = "eng" as const
|
||||
export const CONFIG_SCHEMA_VERSION = 67
|
||||
export const CONFIG_SCHEMA_VERSION = 68
|
||||
|
||||
export const DEFAULT_FLOATING_BUTTON_POSITION = 0.66
|
||||
|
||||
|
|
@ -95,6 +95,7 @@ export const DEFAULT_CONFIG: Config = {
|
|||
position: DEFAULT_FLOATING_BUTTON_POSITION,
|
||||
disabledFloatingButtonPatterns: [],
|
||||
clickAction: "translate",
|
||||
locked: false,
|
||||
},
|
||||
selectionToolbar: {
|
||||
enabled: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue