feat: add single-click copy/paste option and update settings for clipboard behavior

This commit is contained in:
Sergey Kurdin 2025-06-17 18:55:04 -04:00
parent 440d6693fa
commit a7df032f3f
13 changed files with 136 additions and 25 deletions

View File

@ -0,0 +1,5 @@
---
'pastebar-app-ui': patch
---
Added single-click copy/paste option in user preferences

View File

@ -207,6 +207,7 @@ function App() {
isSavedClipsPanelVisibleOnly: settings.isSavedClipsPanelVisibleOnly?.valueBool,
isSimplifiedLayout: settings.isSimplifiedLayout?.valueBool ?? true,
isMainWindowOnTop: settings.isMainWindowOnTop?.valueBool ?? false,
isSingleClickToCopyPaste: settings.isSingleClickToCopyPaste?.valueBool ?? false,
isAppReady: true,
})
settingsStore.initConstants({

View File

@ -45,8 +45,10 @@ function QuickPasteApp() {
i18n.changeLanguage(i18n.resolvedLanguage)
}
// @ts-expect-error
settingsStore.initSettings({
appDataDir: '',
isSingleClickToCopyPaste: settings.isSingleClickToCopyPaste?.valueBool,
appLastUpdateVersion: settings.appLastUpdateVersion?.valueText,
appLastUpdateDate: settings.appLastUpdateDate?.valueText,
isHideMacOSDockIcon: settings.isHideMacOSDockIcon?.valueBool,

View File

@ -38,3 +38,6 @@ Show/Hide Quick Paste Window: Show/Hide Quick Paste Window
Simplified Panel Layout: Simplified Panel Layout
This sets the default icon type for new clips with notes. You can customize individual clips via the context menu.: This sets the default icon type for new clips with notes. You can customize individual clips via the context menu.
When enabled, clicking menu items will only copy content to clipboard instead of auto-pasting. This gives you more control over when and where content is pasted.: When enabled, clicking menu items will only copy content to clipboard instead of auto-pasting. This gives you more control over when and where content is pasted.
? Enable single-click to copy/paste clipboard history items and saved clips instead of requiring double-click.
: Enable single-click to copy/paste clipboard history items and saved clips instead of requiring double-click.
Single Click Copy/Paste: Single Click Copy/Paste

View File

@ -14,19 +14,7 @@ import { MINUTE_IN_MS } from '~/constants'
import { isEmailNotUrl } from '~/libs/utils'
import { formatLocale as format } from '~/locales/date-locales'
import { hoveringHistoryRowId, isKeyAltPressed, isKeyCtrlPressed } from '~/store'
import {
ArrowDownToLine,
Check,
Clipboard,
ClipboardPaste,
Dot,
Grip,
MoreVertical,
MoveDown,
MoveUp,
Star,
X,
} from 'lucide-react'
import { Check, Dot, Star } from 'lucide-react'
import { Highlight, themes } from 'prism-react-renderer'
import { useTranslation } from 'react-i18next'
@ -36,7 +24,7 @@ import ImageWithFallback from '~/components/atoms/image/image-with-fallback-on-e
import LinkCard from '~/components/atoms/link-card/link-card'
import PlayButton from '~/components/atoms/play-button/PlayButton'
import ToolTip from '~/components/atoms/tooltip'
import { Badge, Box, ContextMenu, ContextMenuTrigger, Flex, Text } from '~/components/ui'
import { Badge, Box } from '~/components/ui'
import YoutubeEmbed from '~/components/video-player/YoutubeEmbed'
import { useSignal } from '~/hooks/use-signal'
@ -114,6 +102,7 @@ interface ClipboardHistoryQuickPasteRowProps {
setRowHeight?: (index: number, height: number) => void
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
setAppFilters?: Dispatch<SetStateAction<string[]>>
isSingleClickToCopyPaste?: boolean
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@ -156,26 +145,24 @@ export function ClipboardHistoryQuickPasteRowComponent({
setLargeViewItemId = () => {},
pastingCountDown,
onCopyPaste = () => {},
onCopy = () => {},
invalidateClipboardHistoryQuery = () => {},
generateLinkMetaData,
removeLinkMetaData = () => Promise.resolve(),
isBrokenImage = false,
setExpanded = () => {},
onMovePinnedUpDown = ({}) => {},
setWrapText = () => {},
setBrokenImageItem = () => {},
setSelectHistoryItem = () => {},
isDragPreview = false,
setRowHeight = () => {},
setHistoryFilters = () => {},
setAppFilters = () => {},
isSingleClickToCopyPaste = false,
}: ClipboardHistoryQuickPasteRowProps) {
const { t } = useTranslation()
const rowRef = useRef<HTMLDivElement>(null)
const rowKeyboardRef = useRef<HTMLDivElement>(null)
const isCopiedOrPasted = isCopied || isPasted || isSaved
console.log('isSingleClickToCopyPaste', isSingleClickToCopyPaste)
const contentElementRendered = useSignal<boolean>(false)
const contextMenuOpen = useSignal<boolean>(false)
@ -414,6 +401,9 @@ export function ClipboardHistoryQuickPasteRowComponent({
} else if (largeViewItemId && !isLargeView) {
window.getSelection()?.removeAllRanges()
setLargeViewItemId(clipboard.historyId)
} else if (isSingleClickToCopyPaste && !getSelectedText().text) {
// Single-click in quick paste mode triggers copy+paste
onCopyPaste(clipboard.historyId)
} else {
setKeyboardSelected(clipboard.historyId)
hoveringHistoryRowId.value = !isPinnedTop
@ -430,7 +420,9 @@ export function ClipboardHistoryQuickPasteRowComponent({
hoveringHistoryRowId.value = null
}}
onDoubleClickCapture={e => {
onCopyPaste(clipboard.historyId)
if (!isSingleClickToCopyPaste) {
onCopyPaste(clipboard.historyId)
}
}}
>
<Box

View File

@ -121,6 +121,7 @@ interface ClipboardHistoryRowProps {
setRowHeight?: (index: number, height: number) => void
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
setAppFilters?: Dispatch<SetStateAction<string[]>>
isSingleClickToCopyPaste?: boolean
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@ -178,6 +179,7 @@ export function ClipboardHistoryRowComponent({
setRowHeight = () => {},
setHistoryFilters = () => {},
setAppFilters = () => {},
isSingleClickToCopyPaste = false,
}: ClipboardHistoryRowProps) {
const { t } = useTranslation()
const rowRef = useRef<HTMLDivElement>(null)
@ -434,7 +436,17 @@ export function ClipboardHistoryRowComponent({
}`
}`}
onClickCapture={e => {
if ((isWindows && e.ctrlKey) || (e.metaKey && !isWindows)) {
if (
(isSingleClickToCopyPaste &&
!getSelectedText().text &&
isWindows &&
e.ctrlKey) ||
(e.metaKey && !isWindows)
) {
e.preventDefault()
e.stopPropagation()
onCopyPaste(clipboard.historyId)
} else if ((isWindows && e.ctrlKey) || (e.metaKey && !isWindows)) {
setSelectHistoryItem(clipboard.historyId)
} else if (e.ctrlKey || e.metaKey) {
e.preventDefault()
@ -447,6 +459,25 @@ export function ClipboardHistoryRowComponent({
} else if (largeViewItemId && !isLargeView) {
window.getSelection()?.removeAllRanges()
setLargeViewItemId(clipboard.historyId)
} else if (isSingleClickToCopyPaste && !getSelectedText().text) {
// Check if click is on context menu button or its children
const isContextMenuClick = contextMenuButtonRef.current &&
(contextMenuButtonRef.current.contains(e.target as Node) ||
contextMenuButtonRef.current === e.target)
if (isContextMenuClick) {
return // Don't copy/paste if clicking on context menu
}
if (
e.altKey ||
(e.metaKey && isWindows) ||
(e.ctrlKey && !isWindows)
) {
onCopyPaste(clipboard.historyId)
} else {
onCopy(clipboard.historyId)
}
} else {
hoveringHistoryRowId.value = !isPinnedTop
? clipboard.historyId
@ -462,7 +493,7 @@ export function ClipboardHistoryRowComponent({
hoveringHistoryRowId.value = null
}}
onDoubleClickCapture={e => {
if (!getSelectedText().text) {
if (!isSingleClickToCopyPaste && !getSelectedText().text) {
if (e.altKey || e.metaKey) {
onCopyPaste(clipboard.historyId)
} else {

View File

@ -249,6 +249,7 @@ function DashboardComponent({
clipNotesMaxHeight,
isSimplifiedLayout,
clipNotesMaxWidth,
isSingleClickToCopyPaste,
} = useAtomValue(settingsStoreAtom)
const boardsIds = useMemo(
@ -606,6 +607,7 @@ function DashboardComponent({
showOrganizeLayout.value || isPinnedPanelReorder.value
}
isPinnedBoard={true}
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
/>
))}
</Flex>
@ -1166,6 +1168,7 @@ function DashboardComponent({
isDragPreview
isClipNotesHoverCardsEnabled={false}
isDark={isDark}
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
/>
)}
</DragOverlay>

View File

@ -194,6 +194,7 @@ export function BoardComponent({
clipNotesHoverCardsDelayMS,
clipNotesMaxHeight,
clipNotesMaxWidth,
isSingleClickToCopyPaste,
} = useAtomValue(settingsStoreAtom)
const contextMenuOpen = useSignal(false)
@ -759,6 +760,7 @@ export function BoardComponent({
isKeyboardSelected={
keyboardSelectedClipId?.value === item.id
}
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
/>
)
)}

View File

@ -318,6 +318,7 @@ interface ClipCardProps {
setSelectedItemId?: (id: UniqueIdentifier) => void
isDragPreview?: boolean
isKeyboardSelected?: boolean
isSingleClickToCopyPaste?: boolean
}
export interface ClipDragData {
@ -358,6 +359,7 @@ export function ClipCard({
setShowDetailsItem = () => {},
setSelectedItemId = () => {},
isKeyboardSelected = false,
isSingleClickToCopyPaste = false,
}: ClipCardProps) {
const { t } = useTranslation()
const { isNoteIconsEnabled, defaultNoteIconType } = useAtomValue(settingsStoreAtom)
@ -715,10 +717,30 @@ export function ClipCard({
} else {
setShowDetailsItem(null)
}
} else if (isSingleClickToCopyPaste && !copyDisabled) {
// Check if click is on context menu button or its children
const isContextMenuClick = contextMenuButtonRef.current &&
(contextMenuButtonRef.current.contains(e.target as Node) ||
contextMenuButtonRef.current === e.target)
if (isContextMenuClick) {
return // Don't copy/paste if clicking on context menu
}
// Single-click copy mode
if (e.altKey || e.metaKey) {
if (clip.isForm) {
setPastedItem(clip.id, undefined, true)
} else {
setPastedItem(clip.id)
}
} else {
setCopiedItem(clip.id)
}
}
}}
onDoubleClickCapture={e => {
if (copyDisabled || e.shiftKey) {
if (copyDisabled || e.shiftKey || isSingleClickToCopyPaste) {
e.preventDefault()
return
}

View File

@ -238,6 +238,7 @@ export default function ClipboardHistoryPage() {
isHistoryPanelVisibleOnly,
isSimplifiedLayout,
isSavedClipsPanelVisibleOnly,
isSingleClickToCopyPaste,
} = useAtomValue(settingsStoreAtom)
const { t } = useTranslation()
@ -1488,6 +1489,7 @@ export default function ClipboardHistoryPage() {
clipboard={item}
removeLinkMetaData={removeLinkMetaData}
generateLinkMetaData={generateLinkMetaData}
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
/>
</Box>
)
@ -2111,6 +2113,7 @@ export default function ClipboardHistoryPage() {
clipboard={clipboard}
removeLinkMetaData={removeLinkMetaData}
generateLinkMetaData={generateLinkMetaData}
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
index={index}
style={style}
/>
@ -2170,6 +2173,7 @@ export default function ClipboardHistoryPage() {
: clip.historyId ===
activeDragId.toString().split('::pinned')[0]
})}
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
/>
) : null}
</DragOverlay>

View File

@ -102,8 +102,11 @@ export default function ClipboardHistoryQuickPastePage() {
const isShowSearch = useSignal(false)
const { movePinnedClipboardHistoryUpDown } = useMovePinnedClipboardHistoryUpDown()
const { isAutoPreviewLinkCardsEnabled, isAutoGenerateLinkCardsEnabled } =
useAtomValue(settingsStoreAtom)
const {
isAutoPreviewLinkCardsEnabled,
isAutoGenerateLinkCardsEnabled,
isSingleClickToCopyPaste,
} = useAtomValue(settingsStoreAtom)
const [historyFilters, setHistoryFilters] = useState<string[]>([])
const [codeFilters, setCodeFilters] = useState<string[]>([])
@ -722,6 +725,7 @@ export default function ClipboardHistoryQuickPastePage() {
clipboard={item}
removeLinkMetaData={removeLinkMetaData}
generateLinkMetaData={generateLinkMetaData}
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
/>
)
})}
@ -917,6 +921,7 @@ export default function ClipboardHistoryQuickPastePage() {
clipboard={clipboard}
removeLinkMetaData={removeLinkMetaData}
generateLinkMetaData={generateLinkMetaData}
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
index={index}
style={style}
/>

View File

@ -93,6 +93,8 @@ export default function UserPreferences() {
setIsSavedClipsPanelVisibleOnly,
isSimplifiedLayout,
setIsSimplifiedLayout,
isSingleClickToCopyPaste,
setIsSingleClickToCopyPaste,
} = useAtomValue(settingsStoreAtom)
const { setFontSize, fontSize, setIsSwapPanels, isSwapPanels, returnRoute, isMacOSX } =
@ -725,6 +727,36 @@ export default function UserPreferences() {
</Card>
</Box>
<Box className="animate-in fade-in max-w-xl mt-4">
<Card
className={`${
!isSingleClickToCopyPaste &&
'opacity-80 bg-gray-100 dark:bg-gray-900/80'
}`}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
<CardTitle className="animate-in fade-in text-md font-medium w-full">
{t('Single Click Copy/Paste', { ns: 'settings2' })}
</CardTitle>
<Switch
checked={isSingleClickToCopyPaste}
className="ml-auto"
onCheckedChange={() => {
setIsSingleClickToCopyPaste(!isSingleClickToCopyPaste)
}}
/>
</CardHeader>
<CardContent>
<Text className="text-sm text-muted-foreground">
{t(
'Enable single-click to copy/paste clipboard history items and saved clips instead of requiring double-click.',
{ ns: 'settings2' }
)}
</Text>
</CardContent>
</Card>
</Box>
<Box className="animate-in fade-in max-w-xl mt-4">
<Card
className={`${

View File

@ -97,6 +97,7 @@ type Settings = {
isSavedClipsPanelVisibleOnly: boolean
isSimplifiedLayout: boolean
isMainWindowOnTop: boolean
isSingleClickToCopyPaste: boolean
}
type Constants = {
@ -180,6 +181,7 @@ export interface SettingsStoreState {
setShowBothPanels: (isVisible: boolean) => void
setIsSimplifiedLayout: (isEnabled: boolean) => void
setIsMainWindowOnTop: (isEnabled: boolean) => void
setIsSingleClickToCopyPaste: (isEnabled: boolean) => void
hashPassword: (pass: string) => Promise<string>
isNotTourCompletedOrSkipped: (tourName: string) => boolean
verifyPassword: (pass: string, hash: string) => Promise<boolean>
@ -272,6 +274,7 @@ const initialState: SettingsStoreState & Settings = {
isSavedClipsPanelVisibleOnly: false,
isSimplifiedLayout: true,
isMainWindowOnTop: false,
isSingleClickToCopyPaste: false,
CONST: {
APP_DETECT_LANGUAGES_SUPPORTED: [],
},
@ -337,6 +340,7 @@ const initialState: SettingsStoreState & Settings = {
setShowBothPanels: () => {},
setIsSimplifiedLayout: () => {},
setIsMainWindowOnTop: () => {},
setIsSingleClickToCopyPaste: () => {},
initConstants: () => {},
setAppDataDir: () => {}, // Keep if used for other general app data
setCustomDbPath: () => {},
@ -705,6 +709,11 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
setIsMainWindowOnTop: async (isEnabled: boolean) => {
return get().updateSetting('isMainWindowOnTop', isEnabled)
},
setIsSingleClickToCopyPaste: async (isEnabled: boolean) => {
get().syncStateUpdate('isSingleClickToCopyPaste', isEnabled)
return get().updateSetting('isSingleClickToCopyPaste', isEnabled)
},
isNotTourCompletedOrSkipped: (tourName: string) => {
const { appToursCompletedList, appToursSkippedList } = get()
return (