chore: added changeset

This commit is contained in:
Sergey Kurdin 2025-06-19 12:35:08 -04:00
parent 8afa96590e
commit cdf77523a5
13 changed files with 268 additions and 145 deletions

View File

@ -0,0 +1,5 @@
---
'pastebar-app-ui': patch
---
Added protected collection with PIN access

View File

@ -218,7 +218,9 @@ function App() {
isSingleClickToCopyPaste: settings.isSingleClickToCopyPaste?.valueBool ?? false, isSingleClickToCopyPaste: settings.isSingleClickToCopyPaste?.valueBool ?? false,
hasPinProtectedCollections: hasPinProtectedCollections:
settings.hasPinProtectedCollections?.valueBool ?? false, settings.hasPinProtectedCollections?.valueBool ?? false,
protectedCollections: settings.protectedCollections?.valueText.split(','), protectedCollections: settings.protectedCollections?.valueText
? settings.protectedCollections.valueText.split(',').filter(Boolean)
: [],
isSingleClickToCopyPasteQuickWindow: isSingleClickToCopyPasteQuickWindow:
settings.isSingleClickToCopyPasteQuickWindow?.valueBool ?? false, settings.isSingleClickToCopyPasteQuickWindow?.valueBool ?? false,
isKeepPinnedOnClearEnabled: isKeepPinnedOnClearEnabled:

View File

@ -42,7 +42,7 @@ type Props = {
export default function ModalLockScreenConfirmationWithPasscodeOrPassword({ export default function ModalLockScreenConfirmationWithPasscodeOrPassword({
open, open,
title = 'Unlock Application Screen', title = 'Confirm Passcode',
isLockScreen = false, isLockScreen = false,
showPasscode = true, showPasscode = true,
onConfirmSuccess, onConfirmSuccess,
@ -171,6 +171,16 @@ export default function ModalLockScreenConfirmationWithPasscodeOrPassword({
setFocusField(confirmPasscodeCurrentFocus.value) setFocusField(confirmPasscodeCurrentFocus.value)
}, [confirmPasscodeCurrentFocus.value]) }, [confirmPasscodeCurrentFocus.value])
// Set initial focus when modal opens
useEffect(() => {
if (open && showPasscode && screenLockPassCode) {
setTimeout(() => {
confirmPasscodeCurrentFocus.value = 0
setFocusField(0)
}, 100)
}
}, [open, showPasscode])
return ( return (
<Modal <Modal
open={open} open={open}

View File

@ -1,12 +1,8 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api'
import { collectionsStoreAtom, settingsStoreAtom } from '~/store' import { collectionsStoreAtom } from '~/store'
import { import { useAtomValue } from 'jotai'
isCollectionPinModalOpenAtom,
collectionPinModalPropsAtom,
} from '~/store/uiStore'
import { useAtomValue, useSetAtom } from 'jotai'
import { Collection, Item, Tabs } from '~/types/menu' import { Collection, Item, Tabs } from '~/types/menu'
@ -253,25 +249,11 @@ export function useUpdateCollectionById() {
export function useSelectCollectionById() { export function useSelectCollectionById() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { protectedCollections, screenLockPassCode } =
useAtomValue(settingsStoreAtom)
const { collections, updateCurrentCollectionId } = useAtomValue(collectionsStoreAtom)
const setIsCollectionPinModalOpen = useSetAtom(isCollectionPinModalOpenAtom) const { mutate: selectCollectionById, isSuccess: selectCollectionByIdSuccess } =
const setCollectionPinModalProps = useSetAtom(collectionPinModalPropsAtom)
const { mutate: invokeSelectCollectionById, isSuccess: selectCollectionByIdSuccess } =
useInvokeMutation<Record<string, unknown>, string>('select_collection_by_id', { useInvokeMutation<Record<string, unknown>, string>('select_collection_by_id', {
onSuccess: async (data, variables) => { onSuccess: async data => {
if (data === 'ok') { if (data === 'ok') {
// The actual update to currentCollectionId in collectionsStoreAtom
// might be implicitly handled by backend or needs explicit call here
// For now, assume backend handles state post 'select_collection_by_id'
// and query invalidations refresh the frontend state.
// If direct update is needed:
// const { selectCollection } = variables as { selectCollection: { collectionId: string } };
// updateCurrentCollectionId(selectCollection.collectionId);
await invoke('build_system_menu') await invoke('build_system_menu')
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['get_collections'], queryKey: ['get_collections'],
@ -283,58 +265,14 @@ export function useSelectCollectionById() {
queryKey: ['get_active_collection_with_menu_items'], queryKey: ['get_active_collection_with_menu_items'],
}) })
} else { } else {
console.log('select collection error', data) console.log('update collection error', data)
} }
}, },
}) })
const selectCollectionById = (params: {
selectCollection: { collectionId: string }
}) => {
const { collectionId: targetCollectionId } = params.selectCollection // Renamed for clarity in logs
const isProtected = protectedCollections.includes(targetCollectionId)
const targetCollection = collections.find(c => c.collectionId === targetCollectionId)
if (isProtected && screenLockPassCode && targetCollection) {
console.log(`[Debug] Setting up PIN prompt for collection: ${targetCollectionId}, Title: ${targetCollection.title}`);
const onConfirmSuccessCallback = async () => { // Make callback async
console.log(`[Debug] PIN Confirmed! Attempting to switch to collection via invoke: ${targetCollectionId}`);
try {
// Instead of directly updating UI state, invoke the backend to select the collection.
// The backend will then trigger state updates through query invalidations via the mutation's onSuccess.
await invoke('select_collection_by_id', { collectionId: targetCollectionId });
console.log(`[Debug] Invoke 'select_collection_by_id' for ${targetCollectionId} successful.`);
// Explicitly call query invalidations and menu build,
// as this path is outside the direct TanStack Mutation onSuccess flow.
console.log('[Debug] Manually triggering query invalidations and menu build post-PIN success.');
queryClient.invalidateQueries({ queryKey: ['get_collections'] });
queryClient.invalidateQueries({ queryKey: ['get_active_collection_with_clips'] });
queryClient.invalidateQueries({ queryKey: ['get_active_collection_with_menu_items'] });
await invoke('build_system_menu');
console.log('[Debug] Explicit query invalidations and menu build complete after PIN.');
} catch (error) {
console.error(`[Debug] Error invoking 'select_collection_by_id' for ${targetCollectionId}:`, error);
// Handle error appropriately, e.g., show a toast message to the user
}
}
setCollectionPinModalProps({
title: `Unlock Collection: ${targetCollection.title}`,
onConfirmSuccess: onConfirmSuccessCallback,
})
setIsCollectionPinModalOpen(true)
} else {
// Not protected or no PIN set, proceed as normal
invokeSelectCollectionById(params)
}
}
return { return {
selectCollectionByIdSuccess, selectCollectionByIdSuccess,
selectCollectionById, // This is now our wrapped function selectCollectionById,
} }
} }

View File

@ -21,6 +21,7 @@ import {
openOnBoardingTourName, openOnBoardingTourName,
openOSXSystemPermissionsModal, openOSXSystemPermissionsModal,
openProtectedContentModal, openProtectedContentModal,
pendingProtectedCollectionId,
playerStoreAtom, playerStoreAtom,
resetPassCodeNextDelayInSeconds, resetPassCodeNextDelayInSeconds,
resetPassCodeNumberOfTried, resetPassCodeNumberOfTried,
@ -57,6 +58,7 @@ import ModalLockScreenConfirmationWithPasscodeOrPassword from '~/components/orga
import ModalOSXSystemPermissions from '~/components/organisms/modals/system-permissions-osx-modal' import ModalOSXSystemPermissions from '~/components/organisms/modals/system-permissions-osx-modal'
import { Box, Button, Flex, Text } from '~/components/ui' import { Box, Button, Flex, Text } from '~/components/ui'
import { useSelectCollectionById } from '~/hooks/queries/use-collections'
import { useClipboardPaste, useCopyPaste } from '~/hooks/use-copypaste' import { useClipboardPaste, useCopyPaste } from '~/hooks/use-copypaste'
import { useLocalStorage } from '~/hooks/use-localstorage' import { useLocalStorage } from '~/hooks/use-localstorage'
import { useSignal } from '~/hooks/use-signal' import { useSignal } from '~/hooks/use-signal'
@ -90,6 +92,7 @@ const Container: React.ForwardRefRenderFunction<HTMLDivElement, MainContainerPro
const { historyListSimpleBar, clipboardHistory } = useAtomValue( const { historyListSimpleBar, clipboardHistory } = useAtomValue(
clipboardHistoryStoreAtom clipboardHistoryStoreAtom
) )
const { selectCollectionById } = useSelectCollectionById()
const { const {
appToursCompletedList, appToursCompletedList,
@ -619,17 +622,23 @@ const Container: React.ForwardRefRenderFunction<HTMLDivElement, MainContainerPro
{openProtectedContentModal.value && ( {openProtectedContentModal.value && (
<ModalLockScreenConfirmationWithPasscodeOrPassword <ModalLockScreenConfirmationWithPasscodeOrPassword
open={openProtectedContentModal.value} open={openProtectedContentModal.value}
title={'Test Protected Content'} title={t('Enter PIN to Access Protected Collection', { ns: 'collections' })}
isLockScreen={false} // Important: This makes it cancellable and not the full lock screen isLockScreen={false}
showPasscode={true} // We need PIN (passcode) input showPasscode={true}
onConfirmSuccess={() => { onConfirmSuccess={() => {
openProtectedContentModal.value = false openProtectedContentModal.value = false
// modalProps.onConfirmSuccess() if (pendingProtectedCollectionId.value) {
// setIsOpen(false) selectCollectionById({
// setModalProps(null) // Clear props after success selectCollection: {
collectionId: pendingProtectedCollectionId.value,
},
})
pendingProtectedCollectionId.value = null
}
}} }}
onClose={() => { onClose={() => {
openProtectedContentModal.value = false openProtectedContentModal.value = false
pendingProtectedCollectionId.value = null
}} }}
/> />
)} )}

View File

@ -17,6 +17,8 @@ import {
openAboutPasteBarModal, openAboutPasteBarModal,
openContactUsFormModal, openContactUsFormModal,
openOnBoardingTourName, openOnBoardingTourName,
openProtectedContentModal,
pendingProtectedCollectionId,
playerStoreAtom, playerStoreAtom,
settingsStoreAtom, settingsStoreAtom,
showInvalidTrackWarningAddSong, showInvalidTrackWarningAddSong,
@ -44,6 +46,7 @@ import {
ExternalLink, ExternalLink,
FileCog, FileCog,
LibrarySquare, LibrarySquare,
LockKeyhole,
Maximize, Maximize,
Minus, Minus,
Pause, Pause,
@ -214,6 +217,8 @@ export function NavBar() {
setIsSimplifiedLayout, setIsSimplifiedLayout,
isMainWindowOnTop, isMainWindowOnTop,
setIsMainWindowOnTop, setIsMainWindowOnTop,
hasPinProtectedCollections,
protectedCollections,
} = useAtomValue(settingsStoreAtom) } = useAtomValue(settingsStoreAtom)
const { const {
@ -1264,7 +1269,7 @@ export function NavBar() {
height: 'auto', height: 'auto',
maxHeight: '400px', maxHeight: '400px',
width: '100%', width: '100%',
minWidth: '200px', minWidth: '220px',
}} }}
autoHide={false} autoHide={false}
> >
@ -1291,16 +1296,40 @@ export function NavBar() {
value={collectionId} value={collectionId}
disabled={!isEnabled} disabled={!isEnabled}
onClick={() => { onClick={() => {
selectCollectionById({ const isProtectedCollection =
selectCollection: { hasPinProtectedCollections &&
collectionId, protectedCollections.includes(collectionId)
},
}) if (isProtectedCollection) {
pendingProtectedCollectionId.value = collectionId
openProtectedContentModal.value = true
} else {
selectCollectionById({
selectCollection: {
collectionId,
},
})
}
}} }}
> >
<span className={isSelected ? 'font-semibold' : ''}> <Flex
{title} className={`${
</span> isSelected ? 'font-semibold' : ''
} items-center justify-start gap-2`}
>
{hasPinProtectedCollections &&
protectedCollections.includes(collectionId) ? (
<>
<span className="truncate max-w-[150px]">{title}</span>
<LockKeyhole
size={12}
className="text-gray-600 dark:text-gray-500 flex-shrink-0"
/>
</>
) : (
<span className="truncate max-w-[210px]">{title}</span>
)}
</Flex>
</MenubarRadioItem> </MenubarRadioItem>
))} ))}
</MenubarRadioGroup> </MenubarRadioGroup>

View File

@ -18,9 +18,18 @@ Manage Collections: Manage Collections
Mark collections as protected: Mark collections as protected Mark collections as protected: Mark collections as protected
Pin Protected Collections: Pin Protected Collections Pin Protected Collections: Pin Protected Collections
Protect Collections with PIN: Protect Collections with PIN Protect Collections with PIN: Protect Collections with PIN
Protected Collection: Protected Collection
Select Collections: Select Collections Select Collections: Select Collections
Select protected collections: Select protected collections Select protected collections: Select protected collections
Show collection name on the navbar: Show collection name on the navbar Show collection name on the navbar: Show collection name on the navbar
Switch collections: Switch collections Switch collections: Switch collections
You can add the selected text to your clips or menu. Please select the option below.: You can add the selected text to your clips or menu. Please select the option below. You can add the selected text to your clips or menu. Please select the option below.: You can add the selected text to your clips or menu. Please select the option below.
You need to select a different collection before deleting the current one.: You need to select a different collection before deleting the current one. You need to select a different collection before deleting the current one.: You need to select a different collection before deleting the current one.
Enable PIN Protection: Enable PIN Protection
Disable PIN Protection: Disable PIN Protection
Add To Protected Collections: Add To Protected Collections
Remove From Protected Collections: Remove From Protected Collections
Confirm Add To Protected Collections: Confirm Add To Protected Collections
Confirm Disable PIN Protection: Confirm Disable PIN Protection
Confirm Remove From Protected Collections: Confirm Remove From Protected Collections
Enter PIN to Access Protected Collection: Enter PIN to Access Protected Collection

View File

@ -2,7 +2,8 @@
' This permission ensures PasteBar can access the clipboard and perform copy and paste operations across applications.': ' This permission ensures PasteBar can access the clipboard and perform copy and paste operations across applications.' ' This permission ensures PasteBar can access the clipboard and perform copy and paste operations across applications.': ' This permission ensures PasteBar can access the clipboard and perform copy and paste operations across applications.'
About PasteBar: About PasteBar About PasteBar: About PasteBar
Action Menu: Action Menu Action Menu: Action Menu
Add <b>{{Clipboard}}</b> field to template. This allows you to copy text to the clipboard, and it will be inserted into the template: Add <b>{{Clipboard}}</b> field to template. This allows you to copy text to the clipboard, and it will be inserted into the template ? Add <b>{{Clipboard}}</b> field to template. This allows you to copy text to the clipboard, and it will be inserted into the template
: Add <b>{{Clipboard}}</b> field to template. This allows you to copy text to the clipboard, and it will be inserted into the template
Add Clip: Add Clip Add Clip: Add Clip
Add First Option: Add First Option Add First Option: Add First Option
Add Link Card: Add Link Card Add Link Card: Add Link Card
@ -49,11 +50,14 @@ Close History Window: Close History Window
Close modal and continue in browser: Close modal and continue in browser Close modal and continue in browser: Close modal and continue in browser
Confirm: Confirm Confirm: Confirm
Confirm action: Confirm action Confirm action: Confirm action
Confirm Add To Protected Collections: Confirm Add To Protected Collections
Confirm Delete: Confirm Delete Confirm Delete: Confirm Delete
Confirm Disable PIN Protection: Confirm Disable PIN Protection
Confirm Email: Confirm Email Confirm Email: Confirm Email
Confirm Passcode: Confirm Passcode Confirm Passcode: Confirm Passcode
Confirm Password: Confirm Password Confirm Password: Confirm Password
Confirm Remove: Confirm Remove Confirm Remove: Confirm Remove
Confirm Remove From Protected Collections: Confirm Remove From Protected Collections
Confirm Your Passcode: Confirm Your Passcode Confirm Your Passcode: Confirm Your Passcode
Confirm passcode reset: Confirm passcode reset Confirm passcode reset: Confirm passcode reset
Confirm password reset: Confirm password reset Confirm password reset: Confirm password reset
@ -96,6 +100,7 @@ Enabled: Enabled
Enter Current Passcode: Enter Current Passcode Enter Current Passcode: Enter Current Passcode
Enter Digits Only Passcode: Enter Digits Only Passcode Enter Digits Only Passcode: Enter Digits Only Passcode
Enter Email: Enter Email Enter Email: Enter Email
Enter PIN to Access Protected Collection: Enter PIN to Access Protected Collection
Enter Passcode: Enter Passcode Enter Passcode: Enter Passcode
Enter Password: Enter Password Enter Password: Enter Password
Enter Recovery Password: Enter Recovery Password Enter Recovery Password: Enter Recovery Password
@ -107,7 +112,8 @@ Errors:
Something went wrong! {{err}} Please try again.: Something went wrong! {{err}} Please try again. Something went wrong! {{err}} Please try again.: Something went wrong! {{err}} Please try again.
Expand Edit: Expand Edit Expand Edit: Expand Edit
Expires on {{proExpiresOn}}: Expires on {{proExpiresOn}} Expires on {{proExpiresOn}}: Expires on {{proExpiresOn}}
Field <b>{{Clipboard}}</b> has been found in the template. This allows you to copy text to the clipboard, and it will be inserted into the template: Field <b>{{Clipboard}}</b> has been found in the template. This allows you to copy text to the clipboard, and it will be inserted into the template ? Field <b>{{Clipboard}}</b> has been found in the template. This allows you to copy text to the clipboard, and it will be inserted into the template
: Field <b>{{Clipboard}}</b> has been found in the template. This allows you to copy text to the clipboard, and it will be inserted into the template
Field is not found in the template: Field is not found in the template Field is not found in the template: Field is not found in the template
Find Clip: Find Clip Find Clip: Find Clip
Find History: Find History Find History: Find History
@ -172,7 +178,8 @@ Pasted: Pasted
Path: Path Path: Path
Pause: Pause Pause: Pause
Pause Playing: Pause Playing Pause Playing: Pause Playing
'Permission Check Failed: PasteBar has not been successfully added to Accessibility settings. Please grant the required permissions and click Done again.': 'Permission Check Failed: PasteBar has not been successfully added to Accessibility settings. Please grant the required permissions and click Done again.' ? 'Permission Check Failed: PasteBar has not been successfully added to Accessibility settings. Please grant the required permissions and click Done again.'
: 'Permission Check Failed: PasteBar has not been successfully added to Accessibility settings. Please grant the required permissions and click Done again.'
Pin Selected: Pin Selected Pin Selected: Pin Selected
Pinned: Pinned Pinned: Pinned
Play: Play Play: Play
@ -337,7 +344,8 @@ View Edit: View Edit
Views: Views:
Paste Menu: Paste Menu Paste Menu: Paste Menu
We apologize but you found a bug. Please report this issue to us and try again: We apologize but you found a bug. Please report this issue to us and try again We apologize but you found a bug. Please report this issue to us and try again: We apologize but you found a bug. Please report this issue to us and try again
We couldn't confirm this file's safety. Failed ID3 tag verification and integrity check. MP3 files can potentially contain malware. Please be cautious.: We couldn't confirm this file's safety. Failed ID3 tag verification and integrity check. MP3 files can potentially contain malware. Please be cautious. ? We couldn't confirm this file's safety. Failed ID3 tag verification and integrity check. MP3 files can potentially contain malware. Please be cautious.
: We couldn't confirm this file's safety. Failed ID3 tag verification and integrity check. MP3 files can potentially contain malware. Please be cautious.
Yes: Yes Yes: Yes
Yes, activate: Yes, activate Yes, activate: Yes, activate
chars: chars chars: chars

View File

@ -1,15 +1,19 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import createMenuTree from '~/libs/create-menu-tree' import createMenuTree from '~/libs/create-menu-tree'
import { collectionsStoreAtom, settingsStoreAtom, uiStoreAtom } from '~/store'
import { useAtom, useAtomValue } from 'jotai'
import { import {
CheckSquare, ACTION_TYPE_COMFIRMATION_MODAL,
ChevronDown, actionNameForConfirmModal,
ListFilter, actionTypeConfirmed,
LockKeyhole, actionTypeForConfirmModal,
Trash, collectionsStoreAtom,
Trash2, openActionConfirmModal,
} from 'lucide-react' openProtectedContentModal,
pendingProtectedCollectionId,
settingsStoreAtom,
uiStoreAtom,
} from '~/store'
import { useAtomValue } from 'jotai'
import { CheckSquare, ChevronDown, LockKeyhole, Trash, Trash2 } from 'lucide-react'
// Added ChevronDown, ListFilter // Added ChevronDown, ListFilter
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
@ -102,6 +106,17 @@ export default function ManageCollectionsSection({
const [confirmDeleteCollectionId, setConfirmDeleteCollectionId] = useState< const [confirmDeleteCollectionId, setConfirmDeleteCollectionId] = useState<
string | null string | null
>(null) >(null)
const [pendingProtectionToggle, setPendingProtectionToggle] = useState(false)
const [pendingProtectedCollectionChange, setPendingProtectedCollectionChange] =
useState<{
collectionId: string
checked: boolean
} | null>(null)
// Use action types from constants
const ACTION_TOGGLE_PROTECTION = ACTION_TYPE_COMFIRMATION_MODAL.toggleProtection
const ACTION_CHANGE_PROTECTED_COLLECTIONS =
ACTION_TYPE_COMFIRMATION_MODAL.changeProtectedCollections
useEffect(() => { useEffect(() => {
if (collectionCardEditId) { if (collectionCardEditId) {
@ -123,6 +138,38 @@ export default function ManageCollectionsSection({
setData(menuItems.length > 0 ? createMenuTree(menuItems) : []) setData(menuItems.length > 0 ? createMenuTree(menuItems) : [])
}, [menuItems, menuItems.length]) }, [menuItems, menuItems.length])
// Handle confirmed actions after PIN verification
useEffect(() => {
if (actionTypeConfirmed.value === ACTION_TOGGLE_PROTECTION) {
setHasPinProtectedCollections(pendingProtectionToggle).then(() => {
setPendingProtectionToggle(false)
actionTypeConfirmed.value = null
})
} else if (
actionTypeConfirmed.value === ACTION_CHANGE_PROTECTED_COLLECTIONS &&
pendingProtectedCollectionChange
) {
const currentProtectedIds = [...protectedCollections]
if (pendingProtectedCollectionChange.checked) {
if (
!currentProtectedIds.includes(pendingProtectedCollectionChange.collectionId)
) {
currentProtectedIds.push(pendingProtectedCollectionChange.collectionId)
}
} else {
const index = currentProtectedIds.indexOf(
pendingProtectedCollectionChange.collectionId
)
if (index > -1) {
currentProtectedIds.splice(index, 1)
}
}
setProtectedCollections(currentProtectedIds)
setPendingProtectedCollectionChange(null)
actionTypeConfirmed.value = null
}
}, [actionTypeConfirmed.value])
return ( return (
<AutoSize disableWidth> <AutoSize disableWidth>
{({ height }) => { {({ height }) => {
@ -205,7 +252,7 @@ export default function ManageCollectionsSection({
}} }}
/> />
) : ( ) : (
<Text <Flex
className={`${ className={`${
isSelected isSelected
? isEnabled ? isEnabled
@ -214,19 +261,45 @@ export default function ManageCollectionsSection({
: 'hover:text-slate-500' : 'hover:text-slate-500'
} !font-medium ${ } !font-medium ${
isEnabled ? 'cursor-pointer' : 'text-muted-foreground' isEnabled ? 'cursor-pointer' : 'text-muted-foreground'
}`} } items-center gap-2`}
onClick={() => { onClick={() => {
if (isEnabled && !isSelected) { if (isEnabled && !isSelected) {
selectCollectionById({ // Check if collection is protected
selectCollection: { const isProtectedCollection =
collectionId, hasPinProtectedCollections &&
}, protectedCollections.includes(collectionId)
})
if (isProtectedCollection) {
// Store pending collection and show PIN modal
pendingProtectedCollectionId.value = collectionId
openProtectedContentModal.value = true
} else {
// Switch directly
selectCollectionById({
selectCollection: {
collectionId,
},
})
}
} }
}} }}
> >
{title} <Text className="font-medium truncate">{title}</Text>
</Text> {hasPinProtectedCollections &&
protectedCollections.includes(collectionId) && (
<ToolTip
text={t('Protected Collection', {
ns: 'collections',
})}
isCompact
>
<LockKeyhole
size={15}
className="text-gray-600 dark:text-gray-500 flex-shrink-0"
/>
</ToolTip>
)}
</Flex>
)} )}
</CardTitle> </CardTitle>
{!isCardEdit && isEnabled && ( {!isCardEdit && isEnabled && (
@ -244,11 +317,21 @@ export default function ManageCollectionsSection({
size="xs" size="xs"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
selectCollectionById({ // Check if collection is protected
selectCollection: { const isProtectedCollection =
collectionId, hasPinProtectedCollections &&
}, protectedCollections.includes(collectionId)
})
if (isProtectedCollection) {
pendingProtectedCollectionId.value = collectionId
openProtectedContentModal.value = true
} else {
selectCollectionById({
selectCollection: {
collectionId,
},
})
}
}} }}
> >
{t('Select', { ns: 'common' })} {t('Select', { ns: 'common' })}
@ -445,7 +528,12 @@ export default function ManageCollectionsSection({
</Card> </Card>
{screenLockPassCode && ( {screenLockPassCode && (
<Card> <Card
className={`${
!hasPinProtectedCollections &&
'opacity-80 bg-gray-100 dark:bg-gray-900/80'
}`}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
<CardTitle className="animate-in fade-in text-md font-medium border-red-300 border-1 w-full"> <CardTitle className="animate-in fade-in text-md font-medium border-red-300 border-1 w-full">
<Flex className="flex flex-row items-center justify-start space-y-0 pb-1 gap-2"> <Flex className="flex flex-row items-center justify-start space-y-0 pb-1 gap-2">
@ -454,10 +542,19 @@ export default function ManageCollectionsSection({
</Flex> </Flex>
</CardTitle> </CardTitle>
<Switch <Switch
checked={isShowCollectionNameOnNavBar} checked={hasPinProtectedCollections}
className="ml-auto" className="ml-auto"
onCheckedChange={() => { onCheckedChange={checked => {
setHasPinProtectedCollections(!hasPinProtectedCollections) if (hasPinProtectedCollections) {
setPendingProtectionToggle(checked)
actionNameForConfirmModal.value = checked
? t('Enable PIN Protection', { ns: 'collections' })
: t('Disable PIN Protection', { ns: 'collections' })
actionTypeForConfirmModal.value = ACTION_TOGGLE_PROTECTION
openActionConfirmModal.value = true
} else {
setHasPinProtectedCollections(checked)
}
}} }}
/> />
</CardHeader> </CardHeader>
@ -482,28 +579,48 @@ export default function ManageCollectionsSection({
{collections.map(collection => ( {collections.map(collection => (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
key={collection.collectionId} key={collection.collectionId}
disabled={!hasPinProtectedCollections}
checked={protectedCollections.includes( checked={protectedCollections.includes(
collection.collectionId collection.collectionId
)} )}
onCheckedChange={checked => { onCheckedChange={checked => {
const currentProtectedIds = [...protectedCollections] // If PIN protection is enabled, require PIN to change protected collections
if (checked) { if (hasPinProtectedCollections) {
if ( setPendingProtectedCollectionChange({
!currentProtectedIds.includes( collectionId: collection.collectionId,
checked,
})
actionNameForConfirmModal.value = checked
? t('Add To Protected Collections', {
ns: 'collections',
})
: t('Remove From Protected Collections', {
ns: 'collections',
})
actionTypeForConfirmModal.value =
ACTION_CHANGE_PROTECTED_COLLECTIONS
openActionConfirmModal.value = true
} else {
// If PIN protection is not enabled, allow changes without PIN
const currentProtectedIds = [...protectedCollections]
if (checked) {
if (
!currentProtectedIds.includes(
collection.collectionId
)
) {
currentProtectedIds.push(collection.collectionId)
}
} else {
const index = currentProtectedIds.indexOf(
collection.collectionId collection.collectionId
) )
) { if (index > -1) {
currentProtectedIds.push(collection.collectionId) currentProtectedIds.splice(index, 1)
} }
} else {
const index = currentProtectedIds.indexOf(
collection.collectionId
)
if (index > -1) {
currentProtectedIds.splice(index, 1)
} }
setProtectedCollections(currentProtectedIds)
} }
setProtectedCollections(currentProtectedIds)
}} }}
> >
{collection.title} {collection.title}
@ -512,11 +629,11 @@ export default function ManageCollectionsSection({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Box className="mt-4"> <Box className="mt-4">
<Text className="text-sm font-medium mb-1"> <Text className="text-sm font-medium my-2">
{t('Pin Protected Collections', { ns: 'collections' })}: {t('Pin Protected Collections', { ns: 'collections' })}:
</Text> </Text>
{protectedCollections.length > 0 ? ( {protectedCollections.length > 0 ? (
<Flex className="flex-wrap gap-2"> <Flex className="flex-wrap gap-2 justify-start">
{protectedCollections.map(id => { {protectedCollections.map(id => {
const collection = collections.find( const collection = collections.find(
c => c.collectionId === id c => c.collectionId === id
@ -524,7 +641,7 @@ export default function ManageCollectionsSection({
return collection ? ( return collection ? (
<Badge <Badge
key={id} key={id}
variant="secondary" variant="graySecondary"
className="font-normal" className="font-normal"
> >
{collection.title} {collection.title}

View File

@ -29,6 +29,8 @@ export const CONTENT_TYPE_LANGUAGE = {
export const ACTION_TYPE_COMFIRMATION_MODAL = { export const ACTION_TYPE_COMFIRMATION_MODAL = {
resetPassword: 'RESET_PASSWORD', resetPassword: 'RESET_PASSWORD',
resetPasscode: 'RESET_PASSCODE', resetPasscode: 'RESET_PASSCODE',
toggleProtection: 'TOGGLE_PROTECTION',
changeProtectedCollections: 'CHANGE_PROTECTED_COLLECTIONS',
} as const } as const
export const APP_TOURS = { export const APP_TOURS = {

View File

@ -214,7 +214,7 @@ export interface SettingsStoreState {
setClipTextMinLength: (width: number) => void setClipTextMinLength: (width: number) => void
setClipTextMaxLength: (height: number) => void setClipTextMaxLength: (height: number) => void
setProtectedCollections: (ids: string[]) => void setProtectedCollections: (ids: string[]) => void
setHasPinProtectedCollections: (hasPinProtectedCollections: boolean) => void setHasPinProtectedCollections: (hasPinProtectedCollections: boolean) => Promise<void>
} }
const initialState: SettingsStoreState & Settings = { const initialState: SettingsStoreState & Settings = {
@ -296,7 +296,7 @@ const initialState: SettingsStoreState & Settings = {
isKeepStarredOnClearEnabled: false, isKeepStarredOnClearEnabled: false,
protectedCollections: [], protectedCollections: [],
hasPinProtectedCollections: false, hasPinProtectedCollections: false,
setHasPinProtectedCollections: () => {}, setHasPinProtectedCollections: async () => {},
CONST: { CONST: {
APP_DETECT_LANGUAGES_SUPPORTED: [], APP_DETECT_LANGUAGES_SUPPORTED: [],
}, },
@ -776,8 +776,8 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
setProtectedCollections: async (ids: string[]) => { setProtectedCollections: async (ids: string[]) => {
return get().updateSetting('protectedCollections', ids.join(',')) return get().updateSetting('protectedCollections', ids.join(','))
}, },
setHasPinProtectedCollections: (hasPinProtectedCollections: boolean) => { setHasPinProtectedCollections: async (hasPinProtectedCollections: boolean) => {
return set(() => ({ hasPinProtectedCollections })) return get().updateSetting('hasPinProtectedCollections', hasPinProtectedCollections)
}, },
isNotTourCompletedOrSkipped: (tourName: string) => { isNotTourCompletedOrSkipped: (tourName: string) => {
const { appToursCompletedList, appToursSkippedList } = get() const { appToursCompletedList, appToursSkippedList } = get()

View File

@ -11,7 +11,7 @@ import { ACTION_TYPE_COMFIRMATION_MODAL, APP_TOURS } from './constants'
import { Song, SongSourceType } from './playerStore' import { Song, SongSourceType } from './playerStore'
type ValueOf<T> = T[keyof T] type ValueOf<T> = T[keyof T]
type ActionType = ValueOf<typeof ACTION_TYPE_COMFIRMATION_MODAL> export type ActionType = ValueOf<typeof ACTION_TYPE_COMFIRMATION_MODAL>
export type AppTourType = ValueOf<typeof APP_TOURS> export type AppTourType = ValueOf<typeof APP_TOURS>
export const visibilityCopyPopup = signal(false) export const visibilityCopyPopup = signal(false)
@ -27,6 +27,7 @@ export const openAboutPasteBarModal = signal(false)
export const openContactUsFormModal = signal(false) export const openContactUsFormModal = signal(false)
export const openOnBoardingTourName = signal<AppTourType | null>(null) export const openOnBoardingTourName = signal<AppTourType | null>(null)
export const openProtectedContentModal = signal(false) export const openProtectedContentModal = signal(false)
export const pendingProtectedCollectionId = signal<string | null>(null)
export const onBoardingTourSingleElements = signal<string | string[] | null>(null) export const onBoardingTourSingleElements = signal<string | string[] | null>(null)
export const openOSXSystemPermissionsModal = signal(false) export const openOSXSystemPermissionsModal = signal(false)
export const actionNameForConfirmModal = signal<string | null>(null) export const actionNameForConfirmModal = signal<string | null>(null)

View File

@ -252,10 +252,3 @@ export const uiStore = createStore<UIStoreState>()(
) )
export const uiStoreAtom = atomWithStore(uiStore) export const uiStoreAtom = atomWithStore(uiStore)
// Atoms for Collection PIN Prompt Modal (using LockScreenConfirmationModal)
export const isCollectionPinModalOpenAtom = atom(false)
export const collectionPinModalPropsAtom = atom<{
title: string
onConfirmSuccess: () => void
} | null>(null)