feat: add single-click copy/paste option and update settings for clipboard behavior
This commit is contained in:
parent
440d6693fa
commit
a7df032f3f
5
.changeset/mighty-ears-live.md
Normal file
5
.changeset/mighty-ears-live.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'pastebar-app-ui': patch
|
||||
---
|
||||
|
||||
Added single-click copy/paste option in user preferences
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 => {
|
||||
if (!isSingleClickToCopyPaste) {
|
||||
onCopyPaste(clipboard.historyId)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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={`${
|
||||
|
@ -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 (
|
||||
|
Loading…
x
Reference in New Issue
Block a user