Merge pull request #258 from PasteBar/templates-global-improvments
Global templates handling in clip
This commit is contained in:
commit
2c14a69fc5
5
.changeset/silent-years-drive.md
Normal file
5
.changeset/silent-years-drive.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'pastebar-app-ui': patch
|
||||
---
|
||||
|
||||
Added global templates in user preferences for saved clips
|
@ -227,6 +227,10 @@ function App() {
|
||||
settings.isKeepPinnedOnClearEnabled?.valueBool ?? false,
|
||||
isKeepStarredOnClearEnabled:
|
||||
settings.isKeepStarredOnClearEnabled?.valueBool ?? false,
|
||||
globalTemplatesEnabled: settings.globalTemplatesEnabled?.valueBool ?? true, // Default to true
|
||||
globalTemplates: settings.globalTemplates?.valueText
|
||||
? settings.globalTemplates.valueText // Will be parsed by initSettings in store
|
||||
: [], // Default to empty array
|
||||
isAppReady: true,
|
||||
})
|
||||
settingsStore.initConstants({
|
||||
|
@ -14,6 +14,9 @@ import {
|
||||
} from 'react'
|
||||
import { UniqueIdentifier } from '@dnd-kit/core'
|
||||
import { bbCode } from '~/libs/bbcode'
|
||||
import { settingsStoreAtom } from '~/store'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { Check } from 'lucide-react'
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'
|
||||
|
||||
import { ensureUrlPrefix, escapeRegExp, maskValue } from '~/lib/utils'
|
||||
@ -22,8 +25,9 @@ import { LinkMetadata } from '~/types/history'
|
||||
|
||||
import LinkCard from '../atoms/link-card/link-card'
|
||||
import mergeRefs from '../atoms/merge-refs'
|
||||
import ToolTip from '../atoms/tooltip'
|
||||
import { CardSocialEmbed } from '../social-embend/CardSocialEmbed'
|
||||
import { Box, TextNormal } from '../ui'
|
||||
import { Badge, Box, TextNormal } from '../ui'
|
||||
import YoutubeEmbed from '../video-player/YoutubeEmbed'
|
||||
|
||||
const highlightSearchTermInNode = (
|
||||
@ -99,6 +103,47 @@ const highlightSearchTermInNode = (
|
||||
}
|
||||
}
|
||||
|
||||
const renderWithGlobalTemplateBadges = (
|
||||
value: string,
|
||||
globalTemplates: any[] = []
|
||||
): ReactNode[] => {
|
||||
const templateFieldRegex = /\{\{\s*(.*?)\s*\}\}/g
|
||||
const parts = value.split(templateFieldRegex)
|
||||
|
||||
return parts.map((part: string, index: number): ReactNode => {
|
||||
// Check if this is a template reference (odd indices are matches from regex split)
|
||||
if (index % 2 === 1) {
|
||||
const matchedGlobalTemplate = globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name?.toLowerCase() === part.toLowerCase()
|
||||
)
|
||||
|
||||
if (matchedGlobalTemplate) {
|
||||
// Render as a badge for existing global templates
|
||||
return (
|
||||
<ToolTip
|
||||
key={index}
|
||||
text={matchedGlobalTemplate.value || part}
|
||||
isCompact
|
||||
side="top"
|
||||
className="bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300"
|
||||
>
|
||||
<Badge className="inline-flex items-center gap-1 bg-purple-100 text-purple-700 dark:bg-purple-800 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-700 cursor-default">
|
||||
<Check size={12} className="text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-xs font-medium">{part}</span>
|
||||
</Badge>
|
||||
</ToolTip>
|
||||
)
|
||||
} else {
|
||||
// Template reference doesn't exist - return the full {{templateName}} format
|
||||
return <span key={index}>{`{{${part}}}`}</span>
|
||||
}
|
||||
}
|
||||
|
||||
// Return the plain text part
|
||||
return <span key={index}>{part}</span>
|
||||
})
|
||||
}
|
||||
|
||||
interface CardValueViewerProps {
|
||||
isWrapped: boolean
|
||||
valuePreview: string
|
||||
@ -144,6 +189,24 @@ export const CardValueViewer: FC<CardValueViewerProps> = ({
|
||||
}) => {
|
||||
const highlightedRefs = useRef<React.RefObject<HTMLElement>[]>([])
|
||||
const wrapped = isLink || isVideo || isPath || isWrapped
|
||||
const settings = useAtomValue(settingsStoreAtom)
|
||||
|
||||
// Get global templates from settings
|
||||
const globalTemplatesEnabled = settings.globalTemplatesEnabled || false
|
||||
const globalTemplates = useMemo(() => {
|
||||
if (!globalTemplatesEnabled) return []
|
||||
try {
|
||||
if (typeof settings.globalTemplates === 'string') {
|
||||
return JSON.parse(settings.globalTemplates || '[]')
|
||||
}
|
||||
if (Array.isArray(settings.globalTemplates)) {
|
||||
return settings.globalTemplates
|
||||
}
|
||||
return []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}, [settings.globalTemplates, globalTemplatesEnabled])
|
||||
|
||||
const isTwitter =
|
||||
metadataLinkByItemId?.linkDomain === 'x.com' ||
|
||||
@ -154,17 +217,41 @@ export const CardValueViewer: FC<CardValueViewerProps> = ({
|
||||
|
||||
const valuePreviewParsed = useMemo(() => {
|
||||
if (!isImageData && !isCode && !isImage && valuePreview) {
|
||||
return isMasked
|
||||
const processedValue = isMasked
|
||||
? maskValue(bbCode.remove(valuePreview))
|
||||
: bbCode.parse(valuePreview)
|
||||
|
||||
// Apply global template badges if enabled and templates exist
|
||||
if (
|
||||
globalTemplatesEnabled &&
|
||||
globalTemplates.length > 0 &&
|
||||
typeof processedValue === 'string'
|
||||
) {
|
||||
return renderWithGlobalTemplateBadges(processedValue, globalTemplates)
|
||||
}
|
||||
|
||||
return processedValue
|
||||
}
|
||||
}, [valuePreview])
|
||||
}, [valuePreview, globalTemplatesEnabled, globalTemplates, isMasked])
|
||||
|
||||
const valueParsed = useMemo(() => {
|
||||
if (!isImageData && !isCode && !isImage && textValue) {
|
||||
return isMasked ? maskValue(bbCode.remove(textValue)) : bbCode.parse(textValue)
|
||||
const processedValue = isMasked
|
||||
? maskValue(bbCode.remove(textValue))
|
||||
: bbCode.parse(textValue)
|
||||
|
||||
// Apply global template badges if enabled and templates exist
|
||||
if (
|
||||
globalTemplatesEnabled &&
|
||||
globalTemplates.length > 0 &&
|
||||
typeof processedValue === 'string'
|
||||
) {
|
||||
return renderWithGlobalTemplateBadges(processedValue, globalTemplates)
|
||||
}
|
||||
|
||||
return processedValue
|
||||
}
|
||||
}, [textValue])
|
||||
}, [textValue, globalTemplatesEnabled, globalTemplates, isMasked])
|
||||
|
||||
const highlightedContent = useMemo(() => {
|
||||
if (searchTerm.length > 1) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { createRef, FC, memo, useEffect, useRef, useState } from 'react'
|
||||
import CodeMirror from 'codemirror'
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { escapeRegExp } from '~/lib/utils'
|
||||
|
||||
@ -45,7 +44,6 @@ export const CodeViewer: FC<CodeViewerProps> = ({
|
||||
}) => {
|
||||
const [elements, setElements] = useState<JSX.Element[]>([])
|
||||
const [isModeLoaded, setModeLoaded] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const mdShowFormat = useSignal<'html' | 'markdown'>('html')
|
||||
|
||||
const highlightedRefs = useRef<React.RefObject<HTMLElement>[]>([])
|
||||
|
@ -25,12 +25,10 @@ export default function createFilteredFlatBoardTreeWithClips(
|
||||
const nameMatches = item.name.toLowerCase().includes(lowerFind)
|
||||
|
||||
const valueMatches =
|
||||
!isSearchNameOrLabelOnly &&
|
||||
item.value?.toLowerCase().includes(lowerFind)
|
||||
!isSearchNameOrLabelOnly && item.value?.toLowerCase().includes(lowerFind)
|
||||
|
||||
const descriptionMatches =
|
||||
!isSearchNameOrLabelOnly &&
|
||||
item.description?.toLowerCase().includes(lowerFind)
|
||||
!isSearchNameOrLabelOnly && item.description?.toLowerCase().includes(lowerFind)
|
||||
|
||||
return nameMatches || valueMatches || descriptionMatches
|
||||
})
|
||||
|
@ -21,8 +21,7 @@ Add Section: Abschnitt hinzufügen
|
||||
Add Tab: Tab hinzufügen
|
||||
Add Template Field: Vorlagenfeld hinzufügen
|
||||
Add a Tab: Tab hinzufügen
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Feld <b>{{<b>{{name}}</b>}}</b> zur Vorlage hinzufügen
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Feld <b>{{<b>{{name}}</b>}}</b> zur Vorlage hinzufügen
|
||||
Add field {{name}} into the template: Feld {{name}} zur Vorlage hinzufügen
|
||||
Add image: Bild hinzufügen
|
||||
Add note: Notiz hinzufügen
|
||||
Add to Board: Zum Board hinzufügen
|
||||
@ -83,7 +82,7 @@ Delete field: Feld löschen
|
||||
Delete tab: Tab löschen
|
||||
Detect Template Fields: Vorlagenfelder erkennen
|
||||
Detect for Template Fields: Nach Vorlagenfeldern suchen
|
||||
Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Deaktiviertes Feld <b>{{<b>{{name}}</b>}}</b> wurde in der Vorlage gefunden
|
||||
Disabled field {{name}} has been found in the template: Deaktiviertes Feld {{name}} wurde in der Vorlage gefunden
|
||||
Done Create Clip: Clip-Erstellung abgeschlossen
|
||||
Done Edit: Bearbeitung abgeschlossen
|
||||
Done Edit Tabs: Tab-Bearbeitung abgeschlossen
|
||||
@ -135,7 +134,7 @@ FILTERED_TYPES:
|
||||
RegEx Replace: RegEx Ersetzen
|
||||
Remove Quotes: Anführungszeichen entfernen
|
||||
Field: Feld
|
||||
Field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Feld <b>{{<b>{{name}}</b>}}</b> wurde in der Vorlage gefunden
|
||||
Field {{name}} has been found in the template: Feld {{name}} wurde in der Vorlage gefunden
|
||||
Field Options: Feldoptionen
|
||||
Fields Value: Feldwert
|
||||
File, folder or app path does not exist: Datei-, Ordner- oder App-Pfad existiert nicht
|
||||
|
24
packages/pastebar-app-ui/src/locales/lang/de/templates.yaml
Normal file
24
packages/pastebar-app-ui/src/locales/lang/de/templates.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
Create New Template: Neue Vorlage erstellen
|
||||
Create Template: Vorlage erstellen
|
||||
Enter template content...: Vorlageninhalt eingeben...
|
||||
Enter template name (e.g., "signature", "email"): Vorlagenname eingeben (z.B. "Signatur", "E-Mail")
|
||||
Global: Global
|
||||
Global Template: Globale Vorlage
|
||||
Global Templates: Globale Vorlagen
|
||||
Preview usage: Verwendung vorschau
|
||||
Template Usage: Vorlagen-Verwendung
|
||||
Template name already exists: Vorlagenname existiert bereits. Bitte wählen Sie einen anderen Namen.
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: Der Vorlagenname kann nach der Erstellung nicht geändert werden. Löschen und neu erstellen, um den Namen zu ändern.
|
||||
Use Global Template: Globale Vorlage verwenden
|
||||
addTemplateButton: Vorlage hinzufügen
|
||||
confirmDeleteTemplateMessage: "Sind Sie sicher, dass Sie die globale Vorlage '{{name}}' löschen möchten?"
|
||||
confirmDeleteTemplateTitle: Löschen bestätigen
|
||||
deleteTemplateButtonTooltip: Vorlage löschen
|
||||
enableGlobalTemplatesLabel: Globale Vorlagen aktivieren
|
||||
globalTemplatesDescription: Verwalten Sie wiederverwendbare Textfragmente, die mit {{template_name}} in jeden Clip eingefügt werden können.
|
||||
globalTemplatesTitle: Globale Vorlagen
|
||||
localTemplateConflictWarning: "Eine globale Vorlage namens '{{label}}' existiert ebenfalls. Die lokale Vorlage hat Vorrang innerhalb dieses Clip-Formulars."
|
||||
noGlobalTemplatesYet: Noch keine globalen Vorlagen definiert. Klicken Sie auf 'Vorlage hinzufügen', um eine zu erstellen.
|
||||
templateEnabledLabel: Aktiviert
|
||||
templateNameLabel: Name
|
||||
templateValueLabel: Wert
|
@ -21,8 +21,7 @@ Add Section: Add Section
|
||||
Add Tab: Add Tab
|
||||
Add Template Field: Add Template Field
|
||||
Add a Tab: Add a Tab
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Add field <b>{{<b>{{name}}</b>}}</b> into the template
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Add field <b>{{<b>{{name}}</b>}}</b> into the template
|
||||
Add field {{name}} into the template: Add field {{name}} into the template
|
||||
Add image: Add image
|
||||
Add note: Add note
|
||||
Add to Board: Add to Board
|
||||
@ -83,7 +82,7 @@ Delete field: Delete field
|
||||
Delete tab: Delete tab
|
||||
Detect Template Fields: Detect Template Fields
|
||||
Detect for Template Fields: Detect for Template Fields
|
||||
Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template
|
||||
Disabled field {{name}} has been found in the template: Disabled field {{name}} has been found in the template
|
||||
Done Create Clip: Done Create Clip
|
||||
Done Edit: Done Edit
|
||||
Done Edit Tabs: Done Edit Tabs
|
||||
@ -135,8 +134,8 @@ FILTERED_TYPES:
|
||||
RegEx Replace: RegEx Replace
|
||||
Remove Quotes: Remove Quotes
|
||||
Field: Field
|
||||
Field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Field <b>{{<b>{{name}}</b>}}</b> has been found in the template
|
||||
Field Options: Field Options
|
||||
Field {{name}} has been found in the template: Field {{name}} has been found in the template
|
||||
Fields Value: Fields Value
|
||||
File, folder or app path does not exist: File, folder or app path does not exist
|
||||
File, folder or app path is valid: File, folder or app path is valid
|
||||
|
24
packages/pastebar-app-ui/src/locales/lang/en/templates.yaml
Normal file
24
packages/pastebar-app-ui/src/locales/lang/en/templates.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
Create New Template: Create New Template
|
||||
Create Template: Create Template
|
||||
Enter template content...: Enter template content...
|
||||
Enter template name (e.g., "signature", "email"): Enter template name (e.g., "signature", "email")
|
||||
Global: Global
|
||||
Global Template: Global Template
|
||||
Global Templates: Global Templates
|
||||
Preview usage: Preview usage
|
||||
Template Usage: Template Usage
|
||||
Template name already exists: Template name already exists. Please choose a different name.
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: Template name cannot be changed after creation. Delete and recreate to change name.
|
||||
Use Global Template: Use Global Template
|
||||
addTemplateButton: Add Template
|
||||
confirmDeleteTemplateMessage: Are you sure you want to delete the global template '{{name}}'?
|
||||
confirmDeleteTemplateTitle: Confirm Delete
|
||||
deleteTemplateButtonTooltip: Delete Template
|
||||
enableGlobalTemplatesLabel: Enable Global Templates
|
||||
globalTemplatesDescription: Manage reusable text snippets that can be inserted into any clip using {{template_name}}.
|
||||
globalTemplatesTitle: Global Templates
|
||||
localTemplateConflictWarning: A global template named '{{label}}' also exists. The local template will take precedence within this clip's form.
|
||||
noGlobalTemplatesYet: No global templates defined yet. Click 'Add Template' to create one.
|
||||
templateEnabledLabel: Enabled
|
||||
templateNameLabel: Name
|
||||
templateValueLabel: Value
|
@ -21,8 +21,7 @@ Add Section: Añadir Sección
|
||||
Add Tab: Añadir Pestaña
|
||||
Add Template Field: Añadir Campo de Plantilla
|
||||
Add a Tab: Añadir una Pestaña
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Añadir campo <b>{{<b>{{name}}</b>}}</b> a la plantilla
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Añadir campo <b>{{<b>{{name}}</b>}}</b> a la plantilla
|
||||
Add field {{name}} into the template: Añadir campo {{name}} a la plantilla
|
||||
Add image: Añadir imagen
|
||||
Add note: Añadir nota
|
||||
Add to Board: Añadir al Tablero
|
||||
@ -83,7 +82,7 @@ Delete field: Eliminar campo
|
||||
Delete tab: Eliminar pestaña
|
||||
Detect Template Fields: Detectar Campos de Plantilla
|
||||
Detect for Template Fields: Detectar Campos de Plantilla
|
||||
Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Se ha encontrado el campo deshabilitado <b>{{<b>{{name}}</b>}}</b> en la plantilla
|
||||
Disabled field {{name}} has been found in the template: Se ha encontrado el campo deshabilitado {{name}} en la plantilla
|
||||
Done Create Clip: Clip Creado
|
||||
Done Edit: Edición Completada
|
||||
Done Edit Tabs: Edición de Pestañas Completada
|
||||
@ -135,7 +134,7 @@ FILTERED_TYPES:
|
||||
RegEx Replace: Reemplazo RegEx
|
||||
Remove Quotes: Eliminar Comillas
|
||||
Field: Campo
|
||||
Field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Se ha encontrado el campo <b>{{<b>{{name}}</b>}}</b> en la plantilla
|
||||
Field {{name}} has been found in the template: Se ha encontrado el campo {{name}} en la plantilla
|
||||
Field Options: Opciones de Campo
|
||||
Fields Value: Valor de Campos
|
||||
File, folder or app path does not exist: La ruta del archivo, carpeta o aplicación no existe
|
||||
|
@ -0,0 +1,24 @@
|
||||
Create New Template: Crear Nueva Plantilla
|
||||
Create Template: Crear Plantilla
|
||||
Enter template content...: Introduce el contenido de la plantilla...
|
||||
Enter template name (e.g., "signature", "email"): Introduce el nombre de la plantilla (ej., "firma", "email")
|
||||
Global: Global
|
||||
Global Template: Plantilla Global
|
||||
Global Templates: Plantillas Globales
|
||||
Preview usage: Vista previa de uso
|
||||
Template Usage: Uso de Plantilla
|
||||
Template name already exists: El nombre de la plantilla ya existe. Por favor, elige un nombre diferente.
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: El nombre de la plantilla no se puede cambiar después de la creación. Elimina y vuelve a crear para cambiar el nombre.
|
||||
Use Global Template: Usar Plantilla Global
|
||||
addTemplateButton: Agregar Plantilla
|
||||
confirmDeleteTemplateMessage: ¿Estás seguro de que quieres eliminar la plantilla global '{{name}}'?
|
||||
confirmDeleteTemplateTitle: Confirmar Eliminación
|
||||
deleteTemplateButtonTooltip: Eliminar Plantilla
|
||||
enableGlobalTemplatesLabel: Habilitar Plantillas Globales
|
||||
globalTemplatesDescription: Gestiona fragmentos de texto reutilizables que se pueden insertar en cualquier clip usando {{template_name}}.
|
||||
globalTemplatesTitle: Plantillas Globales
|
||||
localTemplateConflictWarning: También existe una plantilla global llamada '{{label}}'. La plantilla local tendrá prioridad dentro del formulario de este clip.
|
||||
noGlobalTemplatesYet: No hay plantillas globales definidas aún. Haz clic en 'Agregar Plantilla' para crear una.
|
||||
templateEnabledLabel: Habilitado
|
||||
templateNameLabel: Nombre
|
||||
templateValueLabel: Valor
|
@ -21,8 +21,7 @@ Add Section: Ajouter une section
|
||||
Add Tab: Ajouter un onglet
|
||||
Add Template Field: Ajouter un champ de modèle
|
||||
Add a Tab: Ajouter un onglet
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Ajouter le champ <b>{{<b>{{name}}</b>}}</b> dans le modèle
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Ajouter le champ <b>{{<b>{{name}}</b>}}</b> dans le modèle
|
||||
Add field {{name}} into the template: Ajouter le champ {{name}} dans le modèle
|
||||
Add image: Ajouter une image
|
||||
Add note: Ajouter une note
|
||||
Add to Board: Ajouter au tableau
|
||||
@ -83,7 +82,7 @@ Delete field: Supprimer le champ
|
||||
Delete tab: Supprimer l'onglet
|
||||
Detect Template Fields: Détecter les champs du modèle
|
||||
Detect for Template Fields: Détecter les champs du modèle
|
||||
Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Le champ désactivé <b>{{<b>{{name}}</b>}}</b> a été trouvé dans le modèle
|
||||
Disabled field {{name}} has been found in the template: Le champ désactivé {{name}} a été trouvé dans le modèle
|
||||
Done Create Clip: Création du clip terminée
|
||||
Done Edit: Édition terminée
|
||||
Done Edit Tabs: Édition des onglets terminée
|
||||
@ -135,7 +134,7 @@ FILTERED_TYPES:
|
||||
RegEx Replace: Remplacement par expression régulière
|
||||
Remove Quotes: Supprimer les guillemets
|
||||
Field: Champ
|
||||
Field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Le champ <b>{{<b>{{name}}</b>}}</b> a été trouvé dans le modèle
|
||||
Field {{name}} has been found in the template: Le champ {{name}} a été trouvé dans le modèle
|
||||
Field Options: Options du champ
|
||||
Fields Value: Valeur des champs
|
||||
File, folder or app path does not exist: Le chemin du fichier, dossier ou application n'existe pas
|
||||
|
24
packages/pastebar-app-ui/src/locales/lang/fr/templates.yaml
Normal file
24
packages/pastebar-app-ui/src/locales/lang/fr/templates.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
Create New Template: Créer un Nouveau Modèle
|
||||
Create Template: Créer un Modèle
|
||||
Enter template content...: Saisir le contenu du modèle...
|
||||
Enter template name (e.g., "signature", "email"): Saisir le nom du modèle (ex. "signature", "email")
|
||||
Global: Global
|
||||
Global Template: Modèle Global
|
||||
Global Templates: Modèles Globaux
|
||||
Preview usage: Aperçu d'utilisation
|
||||
Template Usage: Utilisation du Modèle
|
||||
Template name already exists: Le nom du modèle existe déjà. Veuillez choisir un nom différent.
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: Le nom du modèle ne peut pas être modifié après création. Supprimez et recréez pour changer le nom.
|
||||
Use Global Template: Utiliser le Modèle Global
|
||||
addTemplateButton: Ajouter un Modèle
|
||||
confirmDeleteTemplateMessage: Êtes-vous sûr de vouloir supprimer le modèle global '{{name}}' ?
|
||||
confirmDeleteTemplateTitle: Confirmer la Suppression
|
||||
deleteTemplateButtonTooltip: Supprimer le Modèle
|
||||
enableGlobalTemplatesLabel: Activer les Modèles Globaux
|
||||
globalTemplatesDescription: Gérez des fragments de texte réutilisables qui peuvent être insérés dans n'importe quel clip en utilisant {{template_name}}.
|
||||
globalTemplatesTitle: Modèles Globaux
|
||||
localTemplateConflictWarning: Un modèle global nommé '{{label}}' existe également. Le modèle local aura la priorité dans le formulaire de ce clip.
|
||||
noGlobalTemplatesYet: Aucun modèle global défini pour le moment. Cliquez sur 'Ajouter un Modèle' pour en créer un.
|
||||
templateEnabledLabel: Activé
|
||||
templateNameLabel: Nom
|
||||
templateValueLabel: Valeur
|
@ -21,8 +21,7 @@ Add Section: Aggiungi Sezione
|
||||
Add Tab: Aggiungi Scheda
|
||||
Add Template Field: Aggiungi Campo Modello
|
||||
Add a Tab: Aggiungi una Scheda
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Aggiungi il campo <b>{{<b>{{name}}</b>}}</b> al modello
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Aggiungi il campo <b>{{<b>{{name}}</b>}}</b> al modello
|
||||
Add field {{name}} into the template: Aggiungi il campo {{name}} al modello
|
||||
Add image: Aggiungi immagine
|
||||
Add note: Aggiungi nota
|
||||
Add to Board: Aggiungi alla Bacheca
|
||||
@ -85,7 +84,7 @@ Delete field: Elimina campo
|
||||
Delete tab: Elimina scheda
|
||||
Detect Template Fields: Rileva Campi del Modello
|
||||
Detect for Template Fields: Rileva Campi del Modello
|
||||
Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Il campo disabilitato <b>{{<b>{{name}}</b>}}</b> è stato trovato nel modello
|
||||
Disabled field {{name}} has been found in the template: Il campo disabilitato {{name}} è stato trovato nel modello
|
||||
Done Create Clip: Creazione Clip Completata
|
||||
Done Edit: Modifica Completata
|
||||
Done Edit Tabs: Modifica Schede Completata
|
||||
@ -137,7 +136,7 @@ FILTERED_TYPES:
|
||||
RegEx Replace: Sostituzione con Espressione Regolare
|
||||
Remove Quotes: Rimuovi Virgolette
|
||||
Field: Campo
|
||||
Field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Il campo <b>{{<b>{{name}}</b>}}</b> è stato trovato nel modello
|
||||
Field {{name}} has been found in the template: Il campo {{name}} è stato trovato nel modello
|
||||
Field Options: Opzioni Campo
|
||||
Fields Value: Valore Campi
|
||||
File, folder or app path does not exist: Il percorso del file, cartella o app non esiste
|
||||
|
24
packages/pastebar-app-ui/src/locales/lang/it/templates.yaml
Normal file
24
packages/pastebar-app-ui/src/locales/lang/it/templates.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
Create New Template: Crea Nuovo Modello
|
||||
Create Template: Crea Modello
|
||||
Enter template content...: Inserisci il contenuto del modello...
|
||||
Enter template name (e.g., "signature", "email"): Inserisci il nome del modello (es. "firma", "email")
|
||||
Global: Globale
|
||||
Global Template: Modello Globale
|
||||
Global Templates: Modelli Globali
|
||||
Preview usage: Anteprima utilizzo
|
||||
Template Usage: Utilizzo del Modello
|
||||
Template name already exists: Il nome del modello esiste già. Scegli un nome diverso.
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: Il nome del modello non può essere modificato dopo la creazione. Elimina e ricrea per cambiare il nome.
|
||||
Use Global Template: Usa Modello Globale
|
||||
addTemplateButton: Aggiungi Modello
|
||||
confirmDeleteTemplateMessage: Sei sicuro di voler eliminare il modello globale '{{name}}'?
|
||||
confirmDeleteTemplateTitle: Conferma Eliminazione
|
||||
deleteTemplateButtonTooltip: Elimina Modello
|
||||
enableGlobalTemplatesLabel: Abilita Modelli Globali
|
||||
globalTemplatesDescription: Gestisci frammenti di testo riutilizzabili che possono essere inseriti in qualsiasi clip usando {{template_name}}.
|
||||
globalTemplatesTitle: Modelli Globali
|
||||
localTemplateConflictWarning: Esiste anche un modello globale chiamato '{{label}}'. Il modello locale avrà la precedenza nel modulo di questo clip.
|
||||
noGlobalTemplatesYet: Nessun modello globale definito ancora. Clicca su 'Aggiungi Modello' per crearne uno.
|
||||
templateEnabledLabel: Abilitato
|
||||
templateNameLabel: Nome
|
||||
templateValueLabel: Valore
|
@ -21,8 +21,7 @@ Add Section: Добавить раздел
|
||||
Add Tab: Добавить вкладку
|
||||
Add Template Field: Добавить поле шаблона
|
||||
Add a Tab: Добавить вкладку
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Добавить поле <b>{{<b>{{name}}</b>}}</b> в шаблон
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Добавить поле <b>{{<b>{{name}}</b>}}</b> в шаблон
|
||||
Add field {{name}} into the template: Добавить поле {{name}} в шаблон
|
||||
Add image: Добавить изображение
|
||||
Add note: Добавить заметку
|
||||
Add to Board: Добавить на доску
|
||||
@ -85,7 +84,7 @@ Delete field: Удалить поле
|
||||
Delete tab: Удалить вкладку
|
||||
Detect Template Fields: Обнаружить поля шаблона
|
||||
Detect for Template Fields: Обнаружить поля шаблона
|
||||
Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Отключенное поле <b>{{<b>{{name}}</b>}}</b> найдено в шаблоне
|
||||
Disabled field {{name}} has been found in the template: Отключенное поле {{name}} найдено в шаблоне
|
||||
Done Create Clip: Завершить
|
||||
Done Edit: Завершить
|
||||
Done Edit Tabs: Завершить
|
||||
@ -137,7 +136,7 @@ FILTERED_TYPES:
|
||||
RegEx Replace: Замена регулярного выражения
|
||||
Remove Quotes: Удалить кавычки
|
||||
Field: Поле
|
||||
Field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Поле <b>{{<b>{{name}}<b>{{<b> найдено в шаблоне
|
||||
Field {{name}} has been found in the template: Поле {{name}} найдено в шаблоне
|
||||
Field Options: Параметры поля
|
||||
Fields Value: Значение полей
|
||||
File, folder or app path does not exist: Путь к файлу, папке или приложению не существует
|
||||
|
24
packages/pastebar-app-ui/src/locales/lang/ru/templates.yaml
Normal file
24
packages/pastebar-app-ui/src/locales/lang/ru/templates.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
Create New Template: Создать Новый Шаблон
|
||||
Create Template: Создать Шаблон
|
||||
Enter template content...: Введите содержимое шаблона...
|
||||
Enter template name (e.g., "signature", "email"): Введите имя шаблона (например, "подпись", "email")
|
||||
Global: Глобальный
|
||||
Global Template: Глобальный Шаблон
|
||||
Global Templates: Глобальные Шаблоны
|
||||
Preview usage: Предварительный просмотр использования
|
||||
Template Usage: Использование Шаблона
|
||||
Template name already exists: Имя шаблона уже существует. Пожалуйста, выберите другое имя.
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: Имя шаблона нельзя изменить после создания. Удалите и создайте заново, чтобы изменить имя.
|
||||
Use Global Template: Использовать Глобальный Шаблон
|
||||
addTemplateButton: Добавить Шаблон
|
||||
confirmDeleteTemplateMessage: Вы уверены, что хотите удалить глобальный шаблон '{{name}}'?
|
||||
confirmDeleteTemplateTitle: Подтвердить Удаление
|
||||
deleteTemplateButtonTooltip: Удалить Шаблон
|
||||
enableGlobalTemplatesLabel: Включить Глобальные Шаблоны
|
||||
globalTemplatesDescription: Управляйте многоразовыми фрагментами текста, которые можно вставить в любой клип, используя {{template_name}}.
|
||||
globalTemplatesTitle: Глобальные Шаблоны
|
||||
localTemplateConflictWarning: Глобальный шаблон с именем '{{label}}' также существует. Локальный шаблон будет иметь приоритет в форме этого клипа.
|
||||
noGlobalTemplatesYet: Глобальные шаблоны пока не определены. Нажмите 'Добавить Шаблон', чтобы создать один.
|
||||
templateEnabledLabel: Включено
|
||||
templateNameLabel: Имя
|
||||
templateValueLabel: Значение
|
@ -21,8 +21,7 @@ Add Section: Bölüm Ekle
|
||||
Add Tab: Sekme Ekle
|
||||
Add Template Field: Şablon Alanı Ekle
|
||||
Add a Tab: Bir Sekme Ekle
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Şablona <b>{{<b>{{name}}</b>}}</b> alan ekle
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Şablona <b>{{<b>{{name}}</b>}}</b> alan ekle
|
||||
Add field {{name}} into the template: Şablona {{name}} alan ekle
|
||||
Add image: Resim Ekle
|
||||
Add note: Not Ekle
|
||||
Add to Board: Panoya Ekle
|
||||
@ -83,7 +82,7 @@ Delete field: Alanı sil
|
||||
Delete tab: Sekmeyi sil
|
||||
Detect Template Fields: Şablon Alanlarını Algıla
|
||||
Detect for Template Fields: Şablon Alanları için Algılama
|
||||
Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Etkisiz Alanlar <b>{{<b>{{name}}</b>}}</b> şablonda bulundu
|
||||
Disabled field {{name}} has been found in the template: Etkisiz Alan {{name}} şablonda bulundu
|
||||
Done Create Clip: Klip Oluşturma Tamamlandı
|
||||
Done Edit: Düzenleme Tamamlandı
|
||||
Done Edit Tabs: Düzenleme Sekmeleri Tamamlandı
|
||||
@ -135,7 +134,7 @@ FILTERED_TYPES:
|
||||
RegEx Replace: RegEx Değiştir
|
||||
Remove Quotes: Remove Alıntılar
|
||||
Field: Alan
|
||||
Field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Alan <b>{{<b>{{name}}</b>}}</b> şablonda bulundu
|
||||
Field {{name}} has been found in the template: Alan {{name}} şablonda bulundu
|
||||
Field Options: Alan Seçenekleri
|
||||
Fields Value: Alan Değeri
|
||||
File, folder or app path does not exist: Dosya, klasör veya uygulama yolu mevcut değil
|
||||
|
24
packages/pastebar-app-ui/src/locales/lang/tr/templates.yaml
Normal file
24
packages/pastebar-app-ui/src/locales/lang/tr/templates.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
Create New Template: Yeni Şablon Oluştur
|
||||
Create Template: Şablon Oluştur
|
||||
Enter template content...: Şablon içeriğini girin...
|
||||
Enter template name (e.g., "signature", "email"): Şablon adını girin (örn. "imza", "e-posta")
|
||||
Global: Global
|
||||
Global Template: Global Şablon
|
||||
Global Templates: Global Şablonlar
|
||||
Preview usage: Kullanım önizlemesi
|
||||
Template Usage: Şablon Kullanımı
|
||||
Template name already exists: Şablon adı zaten mevcut. Lütfen farklı bir ad seçin.
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: Şablon adı oluşturulduktan sonra değiştirilemez. Adı değiştirmek için silin ve yeniden oluşturun.
|
||||
Use Global Template: Global Şablon Kullan
|
||||
addTemplateButton: Şablon Ekle
|
||||
confirmDeleteTemplateMessage: "'{{name}}' global şablonunu silmek istediğinizden emin misiniz?"
|
||||
confirmDeleteTemplateTitle: Silmeyi Onayla
|
||||
deleteTemplateButtonTooltip: Şablonu Sil
|
||||
enableGlobalTemplatesLabel: Global Şablonları Etkinleştir
|
||||
globalTemplatesDescription: "{{template_name}} kullanarak herhangi bir klibe eklenebilen yeniden kullanılabilir metin parçacıklarını yönetin."
|
||||
globalTemplatesTitle: Global Şablonlar
|
||||
localTemplateConflictWarning: "'{{label}}' adlı bir global şablon da mevcut. Yerel şablon bu klibin formu içinde öncelik alacaktır."
|
||||
noGlobalTemplatesYet: Henüz global şablon tanımlanmamış. Bir tane oluşturmak için 'Şablon Ekle'ye tıklayın.
|
||||
templateEnabledLabel: Etkinleştirildi
|
||||
templateNameLabel: Ad
|
||||
templateValueLabel: Değer
|
@ -21,8 +21,7 @@ Add Section: Додати розділ
|
||||
Add Tab: Додати вкладку
|
||||
Add Template Field: Додати поле шаблону
|
||||
Add a Tab: Додати вкладку
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Додати поле <b>{{<b>{{name}}</b>}}</b> в шаблон
|
||||
Add field <b>{{<b>{{name}}</b>}}</b> into the template: Додати поле <b>{{<b>{{name}}</b>}}</b> в шаблон
|
||||
Add field {{name}} into the template: Додати поле {{name}} в шаблон
|
||||
Add image: Додати зображення
|
||||
Add note: Додати примітку
|
||||
Add to Board: Додати на дошку
|
||||
@ -85,7 +84,7 @@ Delete field: Видалити поле
|
||||
Delete tab: Видалити вкладку
|
||||
Detect Template Fields: Виявити поля шаблону
|
||||
Detect for Template Fields: Виявити поля шаблону
|
||||
Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Вимкнене поле <b>{{<b>{{name}}</b>}}</b> знайдено в шаблоні
|
||||
Disabled field {{name}} has been found in the template: Вимкнене поле {{name}} знайдено в шаблоні
|
||||
Done Create Clip: Завершити
|
||||
Done Edit: Завершити
|
||||
Done Edit Tabs: Завершити
|
||||
@ -137,7 +136,7 @@ FILTERED_TYPES:
|
||||
RegEx Replace: Заміна регулярного виразу
|
||||
Remove Quotes: Видалити лапки
|
||||
Field: Поле
|
||||
Field <b>{{<b>{{name}}</b>}}</b> has been found in the template: Поле <b>{{<b>{{name}}<b>{{<b> знайдено в шаблоні
|
||||
Field {{name}} has been found in the template: Поле {{name}} знайдено в шаблоні
|
||||
Field Options: Параметри поля
|
||||
Fields Value: Значення полів
|
||||
File, folder or app path does not exist: Шлях до файлу, папки або додатку не існує
|
||||
|
24
packages/pastebar-app-ui/src/locales/lang/uk/templates.yaml
Normal file
24
packages/pastebar-app-ui/src/locales/lang/uk/templates.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
Create New Template: Створити Новий Шаблон
|
||||
Create Template: Створити Шаблон
|
||||
Enter template content...: Введіть вміст шаблону...
|
||||
Enter template name (e.g., "signature", "email"): Введіть ім'я шаблону (наприклад, "підпис", "email")
|
||||
Global: Глобальний
|
||||
Global Template: Глобальний Шаблон
|
||||
Global Templates: Глобальні Шаблони
|
||||
Preview usage: Попередній перегляд використання
|
||||
Template Usage: Використання Шаблону
|
||||
Template name already exists: Ім'я шаблону вже існує. Будь ласка, оберіть інше ім'я.
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: Ім'я шаблону не можна змінити після створення. Видаліть і створіть заново, щоб змінити ім'я.
|
||||
Use Global Template: Використовувати Глобальний Шаблон
|
||||
addTemplateButton: Додати Шаблон
|
||||
confirmDeleteTemplateMessage: Ви впевнені, що хочете видалити глобальний шаблон '{{name}}'?
|
||||
confirmDeleteTemplateTitle: Підтвердити Видалення
|
||||
deleteTemplateButtonTooltip: Видалити Шаблон
|
||||
enableGlobalTemplatesLabel: Увімкнути Глобальні Шаблони
|
||||
globalTemplatesDescription: Керуйте багаторазовими фрагментами тексту, які можна вставити в будь-який кліп, використовуючи {{template_name}}.
|
||||
globalTemplatesTitle: Глобальні Шаблони
|
||||
localTemplateConflictWarning: Глобальний шаблон з ім'ям '{{label}}' також існує. Локальний шаблон матиме пріоритет у формі цього кліпа.
|
||||
noGlobalTemplatesYet: Глобальні шаблони поки не визначені. Натисніть 'Додати Шаблон', щоб створити один.
|
||||
templateEnabledLabel: Увімкнено
|
||||
templateNameLabel: Ім'я
|
||||
templateValueLabel: Значення
|
@ -21,8 +21,7 @@ Add Section: 添加部分
|
||||
Add Tab: 添加标签
|
||||
Add Template Field: 添加模板字段
|
||||
Add a Tab: 添加一个标签
|
||||
'Add field <b>{{<b>{{name}}</b>}}</b> into the template': '将字段 <b>{{<b>{{name}}</b>}}</b> 添加到模板中'
|
||||
'Add field <b>{{<b>{{name}}</b>}}</b> into the template': 将字段 <b>{{<b>{{name}}</b>}}</b> 添加到模板中
|
||||
Add field {{name}} into the template: 将字段 {{name}} 添加到模板中
|
||||
Add image: 添加图片
|
||||
Add note: 添加注释
|
||||
Add to Board: 添加到面板
|
||||
@ -83,7 +82,7 @@ Delete field: 删除字段
|
||||
Delete tab: 删除标签
|
||||
Detect Template Fields: 检测模板字段
|
||||
Detect for Template Fields: 检测模板字段
|
||||
'Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template': '在模板中找到禁用字段 <b>{{<b>{{name}}</b>}}</b>'
|
||||
Disabled field {{name}} has been found in the template: 在模板中找到禁用字段 {{name}}
|
||||
Done Create Clip: 完成创建剪辑
|
||||
Done Edit: 完成编辑
|
||||
Done Edit Tabs: 完成编辑标签
|
||||
@ -135,7 +134,7 @@ FILTERED_TYPES:
|
||||
RegEx Replace: 正则表达式替换
|
||||
Remove Quotes: 移除引号
|
||||
Field: 字段
|
||||
'Field <b>{{<b>{{name}}</b>}}</b> has been found in the template': '在模板中找到字段 <b>{{<b>{{name}}</b>}}</b>'
|
||||
Field {{name}} has been found in the template: 在模板中找到字段 {{name}}
|
||||
Field Options: 字段选项
|
||||
Fields Value: 字段值
|
||||
File, folder or app path does not exist: 文件、文件夹或应用程序路径不存在
|
||||
|
@ -0,0 +1,24 @@
|
||||
Create New Template: 创建新模板
|
||||
Create Template: 创建模板
|
||||
Enter template content...: 输入模板内容...
|
||||
Enter template name (e.g., "signature", "email"): 输入模板名称(例如:"签名"、"邮件")
|
||||
Global: 全局
|
||||
Global Template: 全局模板
|
||||
Global Templates: 全局模板
|
||||
Preview usage: 预览用法
|
||||
Template Usage: 模板用法
|
||||
Template name already exists: 模板名称已存在。请选择不同的名称。
|
||||
Template name cannot be changed after creation. Delete and recreate to change name.: 创建后无法更改模板名称。删除并重新创建以更改名称。
|
||||
Use Global Template: 使用全局模板
|
||||
addTemplateButton: 添加模板
|
||||
confirmDeleteTemplateMessage: 您确定要删除全局模板 '{{name}}' 吗?
|
||||
confirmDeleteTemplateTitle: 确认删除
|
||||
deleteTemplateButtonTooltip: 删除模板
|
||||
enableGlobalTemplatesLabel: 启用全局模板
|
||||
globalTemplatesDescription: 管理可重复使用的文本片段,可以使用 {{template_name}} 插入到任何剪辑中。
|
||||
globalTemplatesTitle: 全局模板
|
||||
localTemplateConflictWarning: 也存在名为 '{{label}}' 的全局模板。本地模板将在此剪辑的表单中优先使用。
|
||||
noGlobalTemplatesYet: 尚未定义全局模板。点击"添加模板"创建一个。
|
||||
templateEnabledLabel: 已启用
|
||||
templateNameLabel: 名称
|
||||
templateValueLabel: 值
|
@ -181,18 +181,18 @@ export const ClipboardHistoryIconMenu = ({
|
||||
}
|
||||
}
|
||||
if (isRecent) {
|
||||
await clearRecentClipboardHistory({
|
||||
durationType,
|
||||
await clearRecentClipboardHistory({
|
||||
durationType,
|
||||
duration: olderThen,
|
||||
keepPinned: isKeepPinnedOnClearEnabled,
|
||||
keepStarred: isKeepStarredOnClearEnabled
|
||||
keepStarred: isKeepStarredOnClearEnabled,
|
||||
})
|
||||
} else {
|
||||
await clearClipboardHistoryOlderThan({
|
||||
durationType,
|
||||
await clearClipboardHistoryOlderThan({
|
||||
durationType,
|
||||
olderThen,
|
||||
keepPinned: isKeepPinnedOnClearEnabled,
|
||||
keepStarred: isKeepStarredOnClearEnabled
|
||||
keepStarred: isKeepStarredOnClearEnabled,
|
||||
})
|
||||
}
|
||||
setTimeout(() => {
|
||||
|
@ -461,14 +461,15 @@ export function ClipboardHistoryRowComponent({
|
||||
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)
|
||||
|
||||
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) ||
|
||||
|
@ -117,6 +117,7 @@ export type ClipFormTemplateOptions = {
|
||||
defaultValue?: string
|
||||
selectOptions?: string[]
|
||||
isFound?: boolean
|
||||
isGlobal?: boolean
|
||||
type?:
|
||||
| 'password'
|
||||
| 'number'
|
||||
@ -719,14 +720,15 @@ export function ClipCard({
|
||||
}
|
||||
} 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)
|
||||
|
||||
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) {
|
||||
|
@ -12,10 +12,12 @@ import {
|
||||
forceSaveClipNameEditingError,
|
||||
forceSaveEditClipName,
|
||||
isClipNameEditing,
|
||||
settingsStoreAtom,
|
||||
showDeleteImageClipConfirmationId,
|
||||
} from '~/store'
|
||||
import clsx from 'clsx'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import linkifyIt from 'linkify-it'
|
||||
import {
|
||||
AlertTriangle,
|
||||
@ -30,6 +32,7 @@ import {
|
||||
FilePenLine,
|
||||
FileSymlink,
|
||||
FileText,
|
||||
Hash,
|
||||
Heading,
|
||||
Image,
|
||||
Italic,
|
||||
@ -198,6 +201,7 @@ export function ClipEditContent({
|
||||
fields: [],
|
||||
},
|
||||
})
|
||||
const { globalTemplates, globalTemplatesEnabled } = useAtomValue(settingsStoreAtom)
|
||||
const commandTestOutput = useSignal('')
|
||||
const templateTestOutput = useSignal('')
|
||||
const templateTestOutputFormat = useSignal<'text' | 'html'>('text')
|
||||
@ -797,6 +801,56 @@ export function ClipEditContent({
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
{globalTemplatesEnabled && globalTemplates.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Box>
|
||||
<ToolTip
|
||||
isCompact
|
||||
text={t('Global Templates', { ns: 'templates' })}
|
||||
>
|
||||
<Hash
|
||||
size={17}
|
||||
className="hover:text-purple-500 dark:hover:text-purple-400 cursor-pointer"
|
||||
/>
|
||||
</ToolTip>
|
||||
</Box>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" sideOffset={8}>
|
||||
<DropdownMenuItem
|
||||
className="text-center items-center justify-center py-0.5 text-xs"
|
||||
disabled
|
||||
>
|
||||
<Text>{t('Global Templates', { ns: 'templates' })}</Text>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<SimpleBar
|
||||
className="code-filter"
|
||||
style={{ height: 'auto', maxHeight: '260px' }}
|
||||
autoHide={false}
|
||||
>
|
||||
{globalTemplates
|
||||
.filter(template => template.isEnabled)
|
||||
.map((template, idx) => (
|
||||
<DropdownMenuItem
|
||||
key={idx}
|
||||
className="text-xs py-1 cursor-pointer"
|
||||
onClick={() => {
|
||||
// Insert {{templateName}} at cursor position
|
||||
const templateText = `{{${template.name}}}`
|
||||
textAreaRef?.current?.handleAddText(templateText)
|
||||
}}
|
||||
>
|
||||
<Text className="text-purple-600 dark:text-purple-400 font-medium">
|
||||
{template.name}
|
||||
</Text>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</SimpleBar>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<div tabIndex={-1} className="ml-auto mr-0.5">
|
||||
<ToolTip
|
||||
isCompact
|
||||
@ -1744,10 +1798,24 @@ export function ClipEditContent({
|
||||
className="ml-2 px-1.5 h-8 w-8 text-gray-400 border-0 group"
|
||||
onClick={async () => {
|
||||
try {
|
||||
// For global templates, update with current values
|
||||
const updatedTemplateOptions =
|
||||
formTemplateLocalOptions.value.templateOptions.map(field => {
|
||||
if (field.isGlobal && globalTemplatesEnabled) {
|
||||
const globalTemplate = globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name === field.label
|
||||
)
|
||||
return {
|
||||
...field,
|
||||
value: globalTemplate?.value || field.value,
|
||||
}
|
||||
}
|
||||
return field
|
||||
})
|
||||
|
||||
templateTestOutput.value = await invoke('run_template_fill', {
|
||||
templateValue: clipValue.value,
|
||||
templateOptions:
|
||||
formTemplateLocalOptions.value.templateOptions,
|
||||
templateOptions: updatedTemplateOptions,
|
||||
isPreview: true,
|
||||
})
|
||||
} catch (e) {
|
||||
|
@ -1,8 +1,10 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useRef } from 'react'
|
||||
import { arrayMove } from '@dnd-kit/sortable'
|
||||
import { Signal } from '@preact/signals-react'
|
||||
import { Signal, useSignal as useSignalPreact } from '@preact/signals-react' // Renamed to avoid conflict
|
||||
import MaskIcon from '~/assets/icons/mask-square'
|
||||
import { settingsStoreAtom } from '~/store'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import {
|
||||
AlertTriangle,
|
||||
AlignEndVertical,
|
||||
@ -69,17 +71,22 @@ export function ClipEditTemplate({
|
||||
localOptions: Signal<ClipFormTemplateOptions>
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const editFieldId = useSignal<string | null>(null)
|
||||
const editSelectOptionFieldId = useSignal<string | null>(null)
|
||||
const { globalTemplates, globalTemplatesEnabled } = useAtomValue(settingsStoreAtom)
|
||||
const editFieldId = useSignalPreact<string | null>(null)
|
||||
const editSelectOptionFieldId = useSignalPreact<string | null>(null)
|
||||
const addSelectOptionFieldId = useSignal<string | null>(null)
|
||||
const editSelectOptionOriginalValue = useSignal<string | null>(null)
|
||||
const editSelectOptionOriginalValue = useSignalPreact<string | null>(null)
|
||||
const textAreaRef = useRef<TextAreaRef>(null)
|
||||
const showAllLabelsMustBeUniqueMessage = useSignal<boolean>(false)
|
||||
const defaultValueResetKey = useSignal<string>(Date.now().toString())
|
||||
const showAllLabelsMustBeUniqueMessage = useSignalPreact<boolean>(false)
|
||||
const showGlobalConflictWarning = useSignalPreact<boolean>(false)
|
||||
const conflictingGlobalLabel = useSignalPreact<string | null>(null)
|
||||
const defaultValueResetKey = useSignalPreact<string>(Date.now().toString())
|
||||
|
||||
const FIELD_TYPES = ['text', 'textarea', 'select'] as const
|
||||
type FieldType = (typeof FIELD_TYPES)[number]
|
||||
|
||||
// No need to update values - they're fetched dynamically
|
||||
|
||||
return (
|
||||
<Box className="select-none mt-1">
|
||||
<Box className="my-2">
|
||||
@ -108,6 +115,8 @@ export function ClipEditTemplate({
|
||||
key={type}
|
||||
onClick={() => {
|
||||
editFieldId.value = null
|
||||
showGlobalConflictWarning.value = false
|
||||
conflictingGlobalLabel.value = null
|
||||
if (!localOptions.value.templateOptions) {
|
||||
localOptions.value.templateOptions = []
|
||||
}
|
||||
@ -122,14 +131,26 @@ export function ClipEditTemplate({
|
||||
return field
|
||||
})
|
||||
|
||||
const newLabel = `${type.charAt(0).toUpperCase() + type.slice(1)}`
|
||||
newFields.push({
|
||||
id: Date.now().toString(),
|
||||
type,
|
||||
label: `${type.charAt(0).toUpperCase() + type.slice(1)}`,
|
||||
label: newLabel,
|
||||
isEnable: true,
|
||||
value: '',
|
||||
})
|
||||
|
||||
// Check for global conflict
|
||||
if (globalTemplatesEnabled) {
|
||||
const conflictingGlobal = globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name === newLabel
|
||||
)
|
||||
if (conflictingGlobal) {
|
||||
showGlobalConflictWarning.value = true
|
||||
conflictingGlobalLabel.value = newLabel
|
||||
}
|
||||
}
|
||||
|
||||
localOptions.value = {
|
||||
...localOptions.value,
|
||||
templateOptions: [...newFields],
|
||||
@ -167,6 +188,8 @@ export function ClipEditTemplate({
|
||||
}
|
||||
|
||||
showAllLabelsMustBeUniqueMessage.value = false
|
||||
showGlobalConflictWarning.value = false
|
||||
conflictingGlobalLabel.value = null
|
||||
|
||||
const newFields = [...localOptions.value.templateOptions]
|
||||
|
||||
@ -211,6 +234,17 @@ export function ClipEditTemplate({
|
||||
value: '',
|
||||
})
|
||||
|
||||
// Check for global conflict
|
||||
if (globalTemplatesEnabled) {
|
||||
const conflictingGlobal = globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name === newLabel
|
||||
)
|
||||
if (conflictingGlobal) {
|
||||
showGlobalConflictWarning.value = true
|
||||
conflictingGlobalLabel.value = newLabel
|
||||
}
|
||||
}
|
||||
|
||||
localOptions.value = {
|
||||
...localOptions.value,
|
||||
templateOptions: newFields,
|
||||
@ -240,6 +274,8 @@ export function ClipEditTemplate({
|
||||
if (!localOptions.value.templateOptions) {
|
||||
localOptions.value.templateOptions = []
|
||||
}
|
||||
showGlobalConflictWarning.value = false
|
||||
conflictingGlobalLabel.value = null
|
||||
|
||||
const isClipboardFieldExists = localOptions.value.templateOptions?.some(
|
||||
field => field.label === 'Clipboard'
|
||||
@ -263,7 +299,7 @@ export function ClipEditTemplate({
|
||||
newFields.push({
|
||||
id: Date.now().toString(),
|
||||
type: 'text',
|
||||
label: 'Clipboard',
|
||||
label: 'Clipboard', // 'Clipboard' is a special keyword, unlikely to conflict with user global templates
|
||||
isEnable: true,
|
||||
value: '',
|
||||
})
|
||||
@ -281,6 +317,82 @@ export function ClipEditTemplate({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{globalTemplatesEnabled && globalTemplates.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="mini"
|
||||
className="cursor-pointer hover:bg-transparent !px-2 !py-0"
|
||||
>
|
||||
<Text
|
||||
className="!text-purple-500 dark:!text-purple-400 hover:underline"
|
||||
size="xs"
|
||||
>
|
||||
{t('Global Template', { ns: 'templates' })}
|
||||
<ChevronDown size={12} className="ml-1" />
|
||||
</Text>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
sideOffset={12}
|
||||
align="center"
|
||||
className="max-w-[300px]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="text-center items-center justify-center py-0.5 text-xs"
|
||||
disabled={true}
|
||||
>
|
||||
<Text>{t('Global Templates', { ns: 'templates' })}</Text>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<SimpleBar
|
||||
className="code-filter"
|
||||
style={{ height: 'auto', maxHeight: '260px' }}
|
||||
autoHide={false}
|
||||
>
|
||||
{globalTemplates
|
||||
.filter(template => template.isEnabled)
|
||||
.map((template, idx) => (
|
||||
<DropdownMenuItem
|
||||
key={idx}
|
||||
className="text-xs py-1"
|
||||
onClick={() => {
|
||||
// Add global template as a local field with isGlobal flag
|
||||
if (!localOptions.value.templateOptions) {
|
||||
localOptions.value.templateOptions = []
|
||||
}
|
||||
|
||||
const newFields = [...localOptions.value.templateOptions]
|
||||
newFields.push({
|
||||
id: Date.now().toString(),
|
||||
type: 'text',
|
||||
label: template.name,
|
||||
value: '', // Don't store value for global templates
|
||||
isGlobal: true,
|
||||
isEnable: true,
|
||||
})
|
||||
|
||||
localOptions.value = {
|
||||
...localOptions.value,
|
||||
templateOptions: newFields,
|
||||
}
|
||||
|
||||
// Trigger template field check
|
||||
checkForTemplateFieldsCallback()
|
||||
}}
|
||||
>
|
||||
<Flex className="flex items-center justify-between w-full">
|
||||
<Text className="text-purple-600 dark:text-purple-400 font-medium">
|
||||
{template.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</SimpleBar>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@ -350,7 +462,7 @@ export function ClipEditTemplate({
|
||||
<span
|
||||
className={`whitespace-nowrap pr-1 min-w-[80px] overflow-hidden text-ellipsis block ${
|
||||
isLabelOnTop ? 'text-left' : 'text-right max-w-[160px]'
|
||||
}`}
|
||||
} ${field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''}`}
|
||||
>
|
||||
{field.label}
|
||||
</span>
|
||||
@ -365,18 +477,35 @@ export function ClipEditTemplate({
|
||||
if (e.key === 'Enter' || e.key === 'Escape') {
|
||||
editFieldId.value = null
|
||||
showAllLabelsMustBeUniqueMessage.value = false
|
||||
showGlobalConflictWarning.value = false // Reset on action
|
||||
conflictingGlobalLabel.value = null
|
||||
|
||||
const finalLabel = field.label?.trim() || ''
|
||||
|
||||
const isLabelUnique = localOptions.value.templateOptions?.every(
|
||||
(f, index) => {
|
||||
if (index !== i) {
|
||||
return f.label !== field.label
|
||||
return f.label !== finalLabel
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
if (!isLabelUnique) {
|
||||
field.label = `${field.label} ${i + 1}`
|
||||
field.label = `${finalLabel} ${i + 1}`
|
||||
} else {
|
||||
field.label = finalLabel // Ensure trimmed label is set
|
||||
}
|
||||
|
||||
// Check for global conflict
|
||||
if (globalTemplatesEnabled && field.label) {
|
||||
const conflictingGlobal = globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name === field.label
|
||||
)
|
||||
if (conflictingGlobal) {
|
||||
showGlobalConflictWarning.value = true
|
||||
conflictingGlobalLabel.value = field.label
|
||||
}
|
||||
}
|
||||
|
||||
localOptions.value = {
|
||||
@ -395,6 +524,23 @@ export function ClipEditTemplate({
|
||||
className="ml-1 h-8 w-9 text-blue-500 dark:bg-slate-800"
|
||||
onClick={() => {
|
||||
editFieldId.value = null
|
||||
// Final check for conflicts when 'Done' is clicked
|
||||
const finalLabelOnClick = field.label?.trim() || ''
|
||||
if (finalLabelOnClick) {
|
||||
field.label = finalLabelOnClick // Ensure trimmed label is set
|
||||
if (globalTemplatesEnabled) {
|
||||
const conflictingGlobal = globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name === finalLabelOnClick
|
||||
)
|
||||
if (conflictingGlobal) {
|
||||
showGlobalConflictWarning.value = true
|
||||
conflictingGlobalLabel.value = finalLabelOnClick
|
||||
} else {
|
||||
showGlobalConflictWarning.value = false
|
||||
conflictingGlobalLabel.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
localOptions.value = {
|
||||
...localOptions.value,
|
||||
templateOptions: [...localOptions.value.templateOptions],
|
||||
@ -774,24 +920,56 @@ export function ClipEditTemplate({
|
||||
)}
|
||||
</Flex>
|
||||
) : field.label?.toLocaleLowerCase() !== 'clipboard' ? (
|
||||
<InputField
|
||||
small
|
||||
key={defaultValueResetKey.value}
|
||||
placeholder={t('Enter default value', { ns: 'dashboard' })}
|
||||
autoFocus={localOptions.value.templateOptions[i].label !== 'Text'}
|
||||
classNameInput="text-sm border-0 border-b border-gray-200 rounded-none pl-1.5 nowrap overflow-hidden text-ellipsis dark:!text-slate-300 dark:bg-slate-900"
|
||||
disabled={field.isEnable === false}
|
||||
type={field.type === 'number' ? 'number' : 'text'}
|
||||
className={`${
|
||||
field.isEnable === false
|
||||
? 'bg-gray-100 opacity-50 dark:bg-gray-900'
|
||||
: ''
|
||||
} w-full`}
|
||||
onChange={e => {
|
||||
field.value = e.target.value.trim()
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
field.isGlobal ? (
|
||||
// For global templates, show the value but make it non-editable
|
||||
<Flex className="items-center gap-2 w-full">
|
||||
<InputField
|
||||
small
|
||||
key={defaultValueResetKey.value}
|
||||
placeholder=""
|
||||
value={
|
||||
globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name === field.label
|
||||
)?.value || ''
|
||||
}
|
||||
classNameInput="text-sm border-0 border-b border-gray-200 rounded-none pl-1.5 nowrap overflow-hidden text-ellipsis dark:!text-purple-300 dark:bg-slate-900 opacity-75"
|
||||
disabled={true}
|
||||
type="text"
|
||||
className={`${
|
||||
field.isEnable === false
|
||||
? 'bg-gray-100 opacity-50 dark:bg-gray-900'
|
||||
: ''
|
||||
} w-full`}
|
||||
title={`Global Template: ${field.label}`}
|
||||
/>
|
||||
<Badge className="inline-flex items-center gap-1 bg-purple-100 text-purple-700 dark:bg-purple-800 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-700 cursor-default text-xs py-0.5 px-1.5">
|
||||
<Check
|
||||
size={12}
|
||||
className="text-purple-600 dark:text-purple-400"
|
||||
/>
|
||||
{t('Global', { ns: 'templates' })}
|
||||
</Badge>
|
||||
</Flex>
|
||||
) : (
|
||||
<InputField
|
||||
small
|
||||
key={defaultValueResetKey.value}
|
||||
placeholder={t('Enter default value', { ns: 'dashboard' })}
|
||||
autoFocus={localOptions.value.templateOptions[i].label !== 'Text'}
|
||||
classNameInput="text-sm border-0 border-b border-gray-200 rounded-none pl-1.5 nowrap overflow-hidden text-ellipsis dark:!text-slate-300 dark:bg-slate-900"
|
||||
disabled={field.isEnable === false}
|
||||
type={field.type === 'number' ? 'number' : 'text'}
|
||||
className={`${
|
||||
field.isEnable === false
|
||||
? 'bg-gray-100 opacity-50 dark:bg-gray-900'
|
||||
: ''
|
||||
} w-full`}
|
||||
onChange={e => {
|
||||
field.value = e.target.value.trim()
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<InputField
|
||||
@ -960,26 +1138,27 @@ export function ClipEditTemplate({
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{field.label?.toLocaleLowerCase() !== 'clipboard' && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (isEdit) {
|
||||
editFieldId.value = null
|
||||
} else {
|
||||
editFieldId.value = field.id ?? null
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEdit ? (
|
||||
<Text size="xs">{t('Done Edit', { ns: 'common' })}</Text>
|
||||
) : (
|
||||
<Text size="xs">{t('Edit Label', { ns: 'common' })}</Text>
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
<SquarePen size={13} />
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{field.label?.toLocaleLowerCase() !== 'clipboard' &&
|
||||
!field.isGlobal && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (isEdit) {
|
||||
editFieldId.value = null
|
||||
} else {
|
||||
editFieldId.value = field.id ?? null
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEdit ? (
|
||||
<Text size="xs">{t('Done Edit', { ns: 'common' })}</Text>
|
||||
) : (
|
||||
<Text size="xs">{t('Edit Label', { ns: 'common' })}</Text>
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
<SquarePen size={13} />
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{field.isLabelOnTop ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
@ -1077,6 +1256,23 @@ export function ClipEditTemplate({
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
{showGlobalConflictWarning.value && conflictingGlobalLabel.value && (
|
||||
<Text className="!text-orange-800 dark:!text-orange-400 text-[13px] my-2 bg-orange-50 dark:bg-orange-900/70 p-2 relative">
|
||||
<AlertTriangle size={13} className="mr-1 inline-block" />
|
||||
{t('localTemplateConflictWarning', {
|
||||
ns: 'templates',
|
||||
label: conflictingGlobalLabel.value,
|
||||
})}
|
||||
<X
|
||||
className="absolute top-0 right-0 m-2 bg-orange-50 dark:bg-orange-900/70 z-10 cursor-pointer hover:bg-orange-100 dark:hover:bg-orange-800"
|
||||
size={14}
|
||||
onClick={() => {
|
||||
showGlobalConflictWarning.value = false
|
||||
conflictingGlobalLabel.value = null
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
{templateMissingFields.value.length > 0 && (
|
||||
<Flex className="rounded-md gap-2 my-3 items-start justify-start flex-wrap !text-amber-800 dark:!text-amber-400 text-[13px] bg-yellow-50 dark:bg-amber-950 p-2">
|
||||
<Text className="!text-amber-700 dark:!text-amber-500 text-[13px] w-full">
|
||||
@ -1182,19 +1378,19 @@ export function ClipEditTemplate({
|
||||
field.isEnable ? (
|
||||
field.label === 'Clipboard' ? (
|
||||
<Trans
|
||||
i18nKey="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"
|
||||
i18nKey="Field {{Clipboard}} has been found in the template. This allows you to copy text to the clipboard, and it will be inserted into the template"
|
||||
ns="common"
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="Field <b>{{<b>{{name}}</b>}}</b> has been found in the template"
|
||||
i18nKey="Field {{name}} has been found in the template"
|
||||
ns="dashboard"
|
||||
values={{ name: field.label }}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="Disabled field <b>{{<b>{{name}}</b>}}</b> has been found in the template"
|
||||
i18nKey="Disabled field {{name}} has been found in the template"
|
||||
ns="dashboard"
|
||||
values={{ name: field.label }}
|
||||
/>
|
||||
@ -1206,7 +1402,9 @@ export function ClipEditTemplate({
|
||||
<Text
|
||||
className={`${
|
||||
field.isEnable
|
||||
? '!text-green-600 dark:!text-green-400'
|
||||
? field.isGlobal
|
||||
? '!text-purple-600 dark:!text-purple-400'
|
||||
: '!text-green-600 dark:!text-green-400'
|
||||
: '!text-gray-400 dark:!text-gray-500'
|
||||
} !font-normal group`}
|
||||
size="xs"
|
||||
@ -1215,11 +1413,18 @@ export function ClipEditTemplate({
|
||||
variant="outline"
|
||||
className={`${
|
||||
field.isEnable
|
||||
? 'bg-green-100 dark:bg-green-900 hover:bg-green-100/70 dark:hover:bg-green-900 border-green-200 dark:border-green-800'
|
||||
? field.isGlobal
|
||||
? 'bg-purple-100 dark:bg-purple-800 hover:bg-purple-200/70 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800'
|
||||
: 'bg-green-100 dark:bg-green-900 hover:bg-green-100/70 dark:hover:bg-green-900 border-green-200 dark:border-green-800'
|
||||
: 'bg-gray-100 dark:bg-gray-800/70 hover:bg-gray-100/70 dark:hover:bg-gray-700/70 border-gray-200 dark:border-gray-700'
|
||||
} text-normal pr-2.5 group-hover:pr-1.5`}
|
||||
>
|
||||
<Check size={12} className="mr-0.5" />
|
||||
<Check
|
||||
size={12}
|
||||
className={`mr-0.5 ${
|
||||
field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''
|
||||
}`}
|
||||
/>
|
||||
{field.label}
|
||||
<ToolTip
|
||||
text={t('Remove from template', { ns: 'common' })}
|
||||
@ -1287,7 +1492,9 @@ export function ClipEditTemplate({
|
||||
variant="outline"
|
||||
className={`${
|
||||
field.isEnable
|
||||
? 'bg-white dark:bg-slate-300/90 hover:bg-blue-50 dark:hover:bg-blue-300 border-slate-200 dark:border-slate-700 hover:border-blue-200 dark:hover:border-blue-800'
|
||||
? field.isGlobal
|
||||
? 'bg-white dark:bg-slate-300/90 hover:bg-purple-50 dark:hover:bg-purple-300 border-slate-200 dark:border-slate-700 hover:border-purple-200 dark:hover:border-purple-800'
|
||||
: 'bg-white dark:bg-slate-300/90 hover:bg-blue-50 dark:hover:bg-blue-300 border-slate-200 dark:border-slate-700 hover:border-blue-200 dark:hover:border-blue-800'
|
||||
: 'bg-gray-50 dark:bg-gray-800/80 hover:bg-gray-50 dark:hover:bg-gray-800 border-gray-100 dark:border-gray-700'
|
||||
} text-normal pr-2.5`}
|
||||
>
|
||||
|
@ -5,8 +5,9 @@ import { invoke } from '@tauri-apps/api'
|
||||
import { readText } from '@tauri-apps/api/clipboard'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import MaskIcon from '~/assets/icons/mask-square'
|
||||
import { isKeyAltPressed, showEditClipId } from '~/store'
|
||||
import { isKeyAltPressed, settingsStoreAtom, showEditClipId } from '~/store'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Check,
|
||||
@ -64,9 +65,11 @@ const renderWithBadges = (
|
||||
label: string | undefined
|
||||
isValueMasked: boolean | undefined
|
||||
isEnable: boolean | undefined
|
||||
isGlobal?: boolean | undefined
|
||||
}[] = [],
|
||||
clipboardValue: null | string,
|
||||
showValues = false
|
||||
showValues = false,
|
||||
globalTemplates: any[] = []
|
||||
): ReactNode[] => {
|
||||
const templateFieldRegex = /\{\{\s*(.*?)\s*\}\}/g
|
||||
|
||||
@ -77,6 +80,10 @@ const renderWithBadges = (
|
||||
f => f.label?.toLocaleLowerCase() === part.toLocaleLowerCase()
|
||||
)
|
||||
|
||||
const matchedGlobalTemplate = globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name?.toLocaleLowerCase() === part.toLocaleLowerCase()
|
||||
)
|
||||
|
||||
if (matchedField) {
|
||||
const field = {
|
||||
label: part,
|
||||
@ -86,6 +93,7 @@ const renderWithBadges = (
|
||||
isFound: templateFoundFields.includes(part.toLowerCase()),
|
||||
isMissing: templateMissingFields.includes(part.toLowerCase()),
|
||||
isEnable: matchedField.isEnable,
|
||||
isGlobal: matchedField.isGlobal || false,
|
||||
}
|
||||
|
||||
const showValuesInTemplate = showValues && field.value
|
||||
@ -95,7 +103,9 @@ const renderWithBadges = (
|
||||
key={index}
|
||||
className={`${
|
||||
field.isEnable
|
||||
? '!text-green-600 dark:!text-green-400'
|
||||
? field.isGlobal
|
||||
? '!text-purple-600 dark:!text-purple-400'
|
||||
: '!text-green-600 dark:!text-green-400'
|
||||
: '!text-gray-400 dark:!text-gray-600'
|
||||
} !font-normal inline-flex`}
|
||||
size="xs"
|
||||
@ -107,11 +117,13 @@ const renderWithBadges = (
|
||||
variant="outline"
|
||||
className={`${
|
||||
field.isEnable
|
||||
? '!text-green-600 dark:!text-green-400 bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900 border-green-200 dark:border-green-800'
|
||||
? field.isGlobal
|
||||
? '!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800'
|
||||
: '!text-green-600 dark:!text-green-400 bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900 border-green-200 dark:border-green-800'
|
||||
: 'dark:!text-gray-300 text-gray-400 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200/80 dark:border-gray-700/80'
|
||||
} text-normal pr-2.5`}
|
||||
>
|
||||
<Check size={12} className="mr-0.5" />
|
||||
<Check size={12} className={`mr-0.5 ${field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''}`} />
|
||||
{field.label}
|
||||
</Badge>
|
||||
}
|
||||
@ -124,7 +136,9 @@ const renderWithBadges = (
|
||||
variant="outline"
|
||||
className={`${
|
||||
field.isEnable
|
||||
? '!text-green-600 dark:!text-green-400 bg-green-100/80 dark:bg-green-900 hover:bg-green-50/80 dark:hover:bg-green-900/70 border-green-100 hover:border-green-200 dark:border-green-800 dark:hover:border-green-700'
|
||||
? field.isGlobal
|
||||
? '!text-purple-700 dark:!text-purple-300 bg-purple-100/80 dark:bg-purple-800 hover:bg-purple-200/80 dark:hover:bg-purple-700/70 border-purple-100 hover:border-purple-200 dark:border-purple-800 dark:hover:border-purple-700'
|
||||
: '!text-green-600 dark:!text-green-400 bg-green-100/80 dark:bg-green-900 hover:bg-green-50/80 dark:hover:bg-green-900/70 border-green-100 hover:border-green-200 dark:border-green-800 dark:hover:border-green-700'
|
||||
: 'dark:!text-gray-600 text-gray-400 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200/80 dark:border-gray-700/80'
|
||||
} text-[14px] !font-normal px-1 rounded-sm`}
|
||||
>
|
||||
@ -143,11 +157,73 @@ const renderWithBadges = (
|
||||
variant="outline"
|
||||
className={`${
|
||||
field.isEnable
|
||||
? 'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900 border-green-200 dark:border-green-800'
|
||||
? field.isGlobal
|
||||
? '!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800'
|
||||
: 'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900 border-green-200 dark:border-green-800'
|
||||
: 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-200/80 dark:border-gray-700/80'
|
||||
} text-normal pr-2.5`}
|
||||
>
|
||||
<Check size={12} className="mr-0.5" />
|
||||
<Check size={12} className={`mr-0.5 ${field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''}`} />
|
||||
{field.label}
|
||||
</Badge>
|
||||
</ToolTip>
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
} else if (matchedGlobalTemplate) {
|
||||
const field = {
|
||||
label: part,
|
||||
isValueMasked: false,
|
||||
value: matchedGlobalTemplate.value,
|
||||
isFound: true,
|
||||
isMissing: false,
|
||||
isEnable: true,
|
||||
isGlobal: true,
|
||||
}
|
||||
|
||||
const showValuesInTemplate = showValues && field.value
|
||||
|
||||
return (
|
||||
<Text
|
||||
key={index}
|
||||
className="!text-purple-600 dark:!text-purple-400 !font-normal inline-flex"
|
||||
size="xs"
|
||||
>
|
||||
{showValuesInTemplate ? (
|
||||
<ToolTip
|
||||
text={
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800 text-normal pr-2.5"
|
||||
>
|
||||
<Check size={12} className="mr-0.5 text-purple-600 dark:text-purple-400" />
|
||||
{field.label} (Global)
|
||||
</Badge>
|
||||
}
|
||||
className="bg-transparent border-0"
|
||||
side="top"
|
||||
isCompact
|
||||
asChild
|
||||
>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="!text-purple-700 dark:!text-purple-300 bg-purple-100/80 dark:bg-purple-800 hover:bg-purple-200/80 dark:hover:bg-purple-700/70 border-purple-100 hover:border-purple-200 dark:border-purple-800 dark:hover:border-purple-700 text-[14px] !font-normal px-1 rounded-sm"
|
||||
>
|
||||
{field.value}
|
||||
</Badge>
|
||||
</ToolTip>
|
||||
) : (
|
||||
<ToolTip
|
||||
text={`${field.value} (Global Template)`}
|
||||
side="top"
|
||||
isCompact
|
||||
asChild
|
||||
>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800 text-normal pr-2.5"
|
||||
>
|
||||
<Check size={12} className="mr-0.5 text-purple-600 dark:text-purple-400" />
|
||||
{field.label}
|
||||
</Badge>
|
||||
</ToolTip>
|
||||
@ -170,6 +246,7 @@ export function ClipViewTemplate({
|
||||
formTemplateOptions: string | null | undefined
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { globalTemplates, globalTemplatesEnabled } = useAtomValue(settingsStoreAtom)
|
||||
const defaultValueResetKey = useSignal<string>(Date.now().toString())
|
||||
const valueChangeKey = useSignal<string>(Date.now().toString())
|
||||
const showAllLabelsMustBeUniqueMessage = useSignal<boolean>(false)
|
||||
@ -207,18 +284,30 @@ export function ClipViewTemplate({
|
||||
matches.forEach((match, index) => {
|
||||
matches[index] = match.replace(/[\n\r{}]+/g, '').trim()
|
||||
templateFoundFields.value.push(matches[index].toLocaleLowerCase())
|
||||
|
||||
// Check local templates first
|
||||
const field = localOptions.value.templateOptions.find(
|
||||
f => f.label?.toLocaleLowerCase() === matches[index].toLocaleLowerCase()
|
||||
)
|
||||
if (!field) {
|
||||
|
||||
// Check global templates if no local field found
|
||||
const globalTemplate =
|
||||
globalTemplatesEnabled &&
|
||||
globalTemplates.find(
|
||||
gt =>
|
||||
gt.isEnabled &&
|
||||
gt.name?.toLocaleLowerCase() === matches[index].toLocaleLowerCase()
|
||||
)
|
||||
|
||||
if (!field && !globalTemplate) {
|
||||
templateMissingFields.value.push(matches[index])
|
||||
} else {
|
||||
} else if (field) {
|
||||
field.isFound = true
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
[localOptions.value.templateOptions]
|
||||
[localOptions.value.templateOptions, globalTemplates, globalTemplatesEnabled]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -316,14 +405,23 @@ export function ClipViewTemplate({
|
||||
templateMissingFields.value,
|
||||
localOptions.value.templateOptions
|
||||
.filter(f => f.label !== undefined)
|
||||
.map(({ label, isEnable, value, isValueMasked }) => ({
|
||||
label,
|
||||
isValueMasked,
|
||||
value,
|
||||
isEnable,
|
||||
})),
|
||||
.map(({ label, isEnable, value, isValueMasked, isGlobal }) => {
|
||||
// For global templates, get the current value from globalTemplates
|
||||
const actualValue = isGlobal && globalTemplatesEnabled
|
||||
? globalTemplates.find(gt => gt.isEnabled && gt.name === label)?.value || ''
|
||||
: value;
|
||||
|
||||
return {
|
||||
label,
|
||||
isValueMasked,
|
||||
value: actualValue,
|
||||
isEnable,
|
||||
isGlobal,
|
||||
};
|
||||
}),
|
||||
clipboardValueSignal.value,
|
||||
templateShowFormat.value === 'values'
|
||||
templateShowFormat.value === 'values',
|
||||
globalTemplatesEnabled ? globalTemplates : []
|
||||
)
|
||||
}, [
|
||||
value,
|
||||
@ -333,6 +431,8 @@ export function ClipViewTemplate({
|
||||
localOptions.value.templateOptions,
|
||||
clipboardValueSignal.value,
|
||||
templateShowFormat.value,
|
||||
globalTemplates,
|
||||
globalTemplatesEnabled,
|
||||
])
|
||||
|
||||
return (
|
||||
@ -487,6 +587,8 @@ export function ClipViewTemplate({
|
||||
<span
|
||||
className={`whitespace-nowrap pr-1 min-w-[80px] overflow-hidden text-ellipsis block ${
|
||||
isLabelOnTop ? 'text-left' : 'text-right max-w-[160px]'
|
||||
} ${
|
||||
field.isGlobal ? 'text-purple-600 dark:text-purple-400' : ''
|
||||
}`}
|
||||
>
|
||||
{field.label}
|
||||
@ -658,23 +760,50 @@ export function ClipViewTemplate({
|
||||
</DropdownMenu>
|
||||
</Flex>
|
||||
) : field.label?.toLocaleLowerCase() !== 'clipboard' ? (
|
||||
<InputField
|
||||
small
|
||||
key={defaultValueResetKey.value}
|
||||
placeholder={t('Enter field value', { ns: 'dashboard' })}
|
||||
autoFocus={Boolean(localOptions.value.templateOptions[i].label)}
|
||||
classNameInput="text-sm border-0 border-b border-gray-200 rounded-none pl-1.5 nowrap overflow-hidden text-ellipsis dark:!text-slate-300 dark:bg-slate-900"
|
||||
disabled={isEnable}
|
||||
type={field.type === 'number' ? 'number' : 'text'}
|
||||
className={`${
|
||||
isEnable ? 'bg-gray-100 opacity-50 dark:bg-gray-900' : ''
|
||||
} w-full`}
|
||||
onChange={e => {
|
||||
field.value = e.target.value.trim()
|
||||
valueChangeKey.value = Date.now().toString()
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
field.isGlobal ? (
|
||||
// For global templates, show the value but make it non-editable
|
||||
<Flex className="items-center gap-2 w-full">
|
||||
<InputField
|
||||
small
|
||||
key={defaultValueResetKey.value}
|
||||
placeholder=""
|
||||
value={
|
||||
globalTemplates.find(
|
||||
gt => gt.isEnabled && gt.name === field.label
|
||||
)?.value || ''
|
||||
}
|
||||
classNameInput="text-sm border-0 border-b border-gray-200 rounded-none pl-1.5 nowrap overflow-hidden text-ellipsis dark:!text-purple-300 dark:bg-slate-900 opacity-75"
|
||||
disabled={true}
|
||||
type="text"
|
||||
className={`${
|
||||
isEnable ? 'bg-gray-100 opacity-50 dark:bg-gray-900' : ''
|
||||
} w-full`}
|
||||
title={`Global Template: ${field.label}`}
|
||||
/>
|
||||
<Badge className="inline-flex items-center gap-1 bg-purple-100 text-purple-700 dark:bg-purple-800 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-700 cursor-default text-xs py-0.5 px-1.5">
|
||||
<Check size={12} className="text-purple-600 dark:text-purple-400" />
|
||||
{t('Global', { ns: 'templates' })}
|
||||
</Badge>
|
||||
</Flex>
|
||||
) : (
|
||||
<InputField
|
||||
small
|
||||
key={defaultValueResetKey.value}
|
||||
placeholder={t('Enter field value', { ns: 'dashboard' })}
|
||||
autoFocus={Boolean(localOptions.value.templateOptions[i].label)}
|
||||
classNameInput="text-sm border-0 border-b border-gray-200 rounded-none pl-1.5 nowrap overflow-hidden text-ellipsis dark:!text-slate-300 dark:bg-slate-900"
|
||||
disabled={isEnable}
|
||||
type={field.type === 'number' ? 'number' : 'text'}
|
||||
className={`${
|
||||
isEnable ? 'bg-gray-100 opacity-50 dark:bg-gray-900' : ''
|
||||
} w-full`}
|
||||
onChange={e => {
|
||||
field.value = e.target.value.trim()
|
||||
valueChangeKey.value = Date.now().toString()
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<InputField
|
||||
@ -861,13 +990,29 @@ export function ClipViewTemplate({
|
||||
:
|
||||
</Text>
|
||||
{templateMissingFields?.value.map((field, i) => {
|
||||
// Check if this field exists as a global template
|
||||
const globalTemplate =
|
||||
globalTemplatesEnabled &&
|
||||
globalTemplates.find(
|
||||
gt =>
|
||||
gt.isEnabled &&
|
||||
gt.name?.toLocaleLowerCase() === field.toLocaleLowerCase()
|
||||
)
|
||||
|
||||
return (
|
||||
<Box key={i}>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-red-50 !text-red-500 dark:!text-red-400 dark:bg-red-950/80 border-red-100 dark:border-red-900 text-normal px-2"
|
||||
className={`${
|
||||
globalTemplate
|
||||
? 'bg-purple-50 !text-purple-500 dark:!text-purple-400 dark:bg-purple-950/80 border-purple-100 dark:border-purple-900'
|
||||
: 'bg-red-50 !text-red-500 dark:!text-red-400 dark:bg-red-950/80 border-red-100 dark:border-red-900'
|
||||
} text-normal px-2`}
|
||||
>
|
||||
{field}
|
||||
{globalTemplate && (
|
||||
<Text className="ml-1 text-xs opacity-70">(Global)</Text>
|
||||
)}
|
||||
</Badge>
|
||||
</Box>
|
||||
)
|
||||
|
@ -527,12 +527,14 @@ export default function ClipboardHistoryQuickPastePage() {
|
||||
// If search is active and input is focused, only process navigation keys
|
||||
if (isShowSearch.value && document.activeElement === searchHistoryInputRef?.current) {
|
||||
// Allow navigation keys to be processed
|
||||
if (!keyUp.includes(event.key) &&
|
||||
!keyDown.includes(event.key) &&
|
||||
!keyPageUp.includes(event.key) &&
|
||||
!keyPageDown.includes(event.key) &&
|
||||
!keyHome.includes(event.key) &&
|
||||
!keyEnter.includes(event.key)) {
|
||||
if (
|
||||
!keyUp.includes(event.key) &&
|
||||
!keyDown.includes(event.key) &&
|
||||
!keyPageUp.includes(event.key) &&
|
||||
!keyPageDown.includes(event.key) &&
|
||||
!keyHome.includes(event.key) &&
|
||||
!keyEnter.includes(event.key)
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -1106,13 +1108,15 @@ const SearchInput = React.memo(
|
||||
className="text-md ring-offset-0 bg-slate-100 dark:bg-slate-700 border-r-0 border-t-0 border-b-0"
|
||||
onKeyDown={e => {
|
||||
// Allow navigation keys and Escape to bubble up
|
||||
if (!keyEscape.includes(e.key) &&
|
||||
!keyUp.includes(e.key) &&
|
||||
!keyDown.includes(e.key) &&
|
||||
!keyPageUp.includes(e.key) &&
|
||||
!keyPageDown.includes(e.key) &&
|
||||
!keyHome.includes(e.key) &&
|
||||
!keyEnter.includes(e.key)) {
|
||||
if (
|
||||
!keyEscape.includes(e.key) &&
|
||||
!keyUp.includes(e.key) &&
|
||||
!keyDown.includes(e.key) &&
|
||||
!keyPageUp.includes(e.key) &&
|
||||
!keyPageDown.includes(e.key) &&
|
||||
!keyHome.includes(e.key) &&
|
||||
!keyEnter.includes(e.key)
|
||||
) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}}
|
||||
|
@ -168,11 +168,11 @@ export default function PasteMenuPage() {
|
||||
const menuItemFound = menuItems.find(menuItem => menuItem.itemId === item.id)
|
||||
|
||||
if (menuItemFound && isMatchOrHasMatchingDescendant) {
|
||||
const newItem = {
|
||||
...menuItemFound,
|
||||
indent: depth,
|
||||
const newItem = {
|
||||
...menuItemFound,
|
||||
indent: depth,
|
||||
id: item.id,
|
||||
hasChildren: item.children && item.children.length > 0
|
||||
hasChildren: item.children && item.children.length > 0,
|
||||
}
|
||||
flatList.push(newItem)
|
||||
|
||||
|
@ -0,0 +1,296 @@
|
||||
import { useState } from 'react'
|
||||
import { confirm } from '@tauri-apps/api/dialog'
|
||||
import { CopyComponent } from '~/libs/bbcode'
|
||||
import { settingsStoreAtom } from '~/store'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { Check, Plus, Save, Trash2, X } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import ToolTip from '~/components/atoms/tooltip'
|
||||
import InputField from '~/components/molecules/input'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Flex,
|
||||
Switch,
|
||||
Text,
|
||||
} from '~/components/ui'
|
||||
|
||||
interface NewTemplate {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export default function GlobalTemplatesSettings() {
|
||||
const { t } = useTranslation()
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [newTemplate, setNewTemplate] = useState<NewTemplate>({ name: '', value: '' })
|
||||
const [nameError, setNameError] = useState<string | null>(null)
|
||||
|
||||
const {
|
||||
globalTemplatesEnabled,
|
||||
setGlobalTemplatesEnabled,
|
||||
globalTemplates,
|
||||
addGlobalTemplate,
|
||||
updateGlobalTemplate,
|
||||
deleteGlobalTemplate,
|
||||
toggleGlobalTemplateEnabledState,
|
||||
} = useAtomValue(settingsStoreAtom)
|
||||
|
||||
const handleCreateTemplate = () => {
|
||||
if (newTemplate.name.trim() && newTemplate.value.trim()) {
|
||||
// Check for duplicate template name
|
||||
const isDuplicate = globalTemplates?.some(
|
||||
template => template.name.toLowerCase() === newTemplate.name.trim().toLowerCase()
|
||||
)
|
||||
|
||||
if (isDuplicate) {
|
||||
setNameError(t('Template name already exists', { ns: 'templates' }))
|
||||
return
|
||||
}
|
||||
|
||||
addGlobalTemplate({
|
||||
name: newTemplate.name.trim(),
|
||||
value: newTemplate.value.trim(),
|
||||
})
|
||||
setNewTemplate({ name: '', value: '' })
|
||||
setNameError(null)
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelCreate = () => {
|
||||
setNewTemplate({ name: '', value: '' })
|
||||
setNameError(null)
|
||||
setIsCreating(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="animate-in fade-in max-w-xl mt-4">
|
||||
<Card>
|
||||
<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('globalTemplatesTitle', { ns: 'templates' })}
|
||||
</CardTitle>
|
||||
<Switch
|
||||
checked={globalTemplatesEnabled}
|
||||
className="ml-auto"
|
||||
onCheckedChange={setGlobalTemplatesEnabled}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
{t('globalTemplatesDescription', { ns: 'templates' })}
|
||||
</Text>
|
||||
|
||||
{globalTemplatesEnabled && (
|
||||
<Box className="mt-4">
|
||||
{globalTemplates && globalTemplates.length > 0 && (
|
||||
<Box className="space-y-3 mb-4">
|
||||
{globalTemplates.map(template => (
|
||||
<Box
|
||||
key={template.id}
|
||||
className={`p-3 border rounded-lg ${
|
||||
template.isEnabled
|
||||
? 'bg-slate-50 dark:bg-slate-900/50'
|
||||
: 'bg-gray-100 dark:bg-gray-800/50 opacity-70'
|
||||
}`}
|
||||
>
|
||||
<Flex className="items-start gap-3">
|
||||
<Box className="flex-1">
|
||||
<Flex className="items-center gap-3">
|
||||
<ToolTip
|
||||
text={t(
|
||||
'Template name cannot be changed after creation. Delete and recreate to change name.',
|
||||
{ ns: 'templates' }
|
||||
)}
|
||||
sideOffset={5}
|
||||
isCompact
|
||||
side="bottom"
|
||||
>
|
||||
{template.isEnabled ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="!text-purple-700 dark:!text-purple-300 bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-200 dark:border-purple-800 text-normal pr-2.5 flex-shrink-0"
|
||||
>
|
||||
<Check
|
||||
size={12}
|
||||
className="mr-0.5 text-purple-600 dark:text-purple-400"
|
||||
/>
|
||||
{template.name}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="!text-gray-700 dark:!text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 border-gray-200 dark:border-gray-800 text-normal pr-2.5 flex-shrink-0"
|
||||
>
|
||||
<X
|
||||
size={12}
|
||||
className="mr-0.5 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
{template.name}
|
||||
</Badge>
|
||||
)}
|
||||
</ToolTip>
|
||||
<InputField
|
||||
small
|
||||
disabled={!template.isEnabled}
|
||||
placeholder={t('templateValueLabel', { ns: 'templates' })}
|
||||
defaultValue={template.value}
|
||||
classNameInput={`text-sm border-0 border-b border-gray-200 rounded-none pl-1.5 bg-transparent ${
|
||||
!template.isEnabled
|
||||
? '!text-gray-500 dark:!text-gray-600'
|
||||
: 'dark:!text-slate-300'
|
||||
}`}
|
||||
className="flex-1"
|
||||
onBlur={e =>
|
||||
updateGlobalTemplate({
|
||||
id: template.id,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<Flex className="flex-col items-center gap-2">
|
||||
<Switch
|
||||
title={t('Enable / Disable', { ns: 'common' })}
|
||||
checked={template.isEnabled}
|
||||
onCheckedChange={() =>
|
||||
toggleGlobalTemplateEnabledState(template.id)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{template.name && (
|
||||
<Flex className="mt-2 pt-1 justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-500 hover:text-red-600 px-1.5 py-0.5"
|
||||
title={t('deleteTemplateButtonTooltip', { ns: 'templates' })}
|
||||
onClick={async () => {
|
||||
const confirmed = await confirm(
|
||||
t('confirmDeleteTemplateMessage', {
|
||||
ns: 'templates',
|
||||
name: template.name,
|
||||
}),
|
||||
{
|
||||
title: t('confirmDeleteTemplateTitle', {
|
||||
ns: 'templates',
|
||||
}),
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
if (confirmed) {
|
||||
deleteGlobalTemplate(template.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
<Box />
|
||||
<Flex className="gap-2 items-center text-sm">
|
||||
<Text className="text-xs text-muted-foreground">
|
||||
{t('Template Usage', { ns: 'templates' })}:
|
||||
</Text>
|
||||
<CopyComponent
|
||||
text={`{{${template.name}}}`}
|
||||
copyText={`{{${template.name}}}`}
|
||||
id={parseInt(template.id, 10)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Create New Template Section */}
|
||||
{!isCreating ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
{t('addTemplateButton', { ns: 'templates' })}
|
||||
</Button>
|
||||
) : (
|
||||
<Box className="p-4 border border-dashed border-blue-300 dark:border-blue-600 rounded-lg bg-blue-50/50 dark:bg-blue-900/20">
|
||||
<Text className="text-sm font-medium mb-3 text-blue-800 dark:text-blue-200">
|
||||
{t('Create New Template', { ns: 'templates' })}
|
||||
</Text>
|
||||
|
||||
<Box className="space-y-3">
|
||||
<InputField
|
||||
small
|
||||
label={t('templateNameLabel', { ns: 'templates' })}
|
||||
placeholder={t('Enter template name (e.g., "signature", "email")', {
|
||||
ns: 'templates',
|
||||
})}
|
||||
value={newTemplate.name}
|
||||
onChange={e => {
|
||||
setNewTemplate(prev => ({ ...prev, name: e.target.value }))
|
||||
// Clear error when user types
|
||||
if (nameError) setNameError(null)
|
||||
}}
|
||||
error={nameError as string}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<InputField
|
||||
small
|
||||
label={t('templateValueLabel', { ns: 'templates' })}
|
||||
placeholder={t('Enter template content...', { ns: 'templates' })}
|
||||
value={newTemplate.value}
|
||||
onChange={e =>
|
||||
setNewTemplate(prev => ({ ...prev, value: e.target.value }))
|
||||
}
|
||||
/>
|
||||
|
||||
{newTemplate.name.trim() && (
|
||||
<Box className="p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs">
|
||||
<Text className="text-muted-foreground mb-1">
|
||||
{t('Preview usage', { ns: 'templates' })}:
|
||||
</Text>
|
||||
<Text className="text-blue-600 dark:text-blue-400 text-sm">
|
||||
{`{{${newTemplate.name.trim()}}}`}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Flex className="gap-2 justify-end">
|
||||
<Button variant="ghost" size="sm" onClick={handleCancelCreate}>
|
||||
<X size={18} className="mr-1" />
|
||||
{t('Cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleCreateTemplate}
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-1.5 px-3 rounded-lg"
|
||||
disabled={!newTemplate.name.trim() || !newTemplate.value.trim()}
|
||||
>
|
||||
<Check size={18} className="mr-1" />
|
||||
{t('Create Template', { ns: 'templates' })}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { confirm } from '@tauri-apps/api/dialog'
|
||||
import { CopyComponent } from '~/libs/bbcode'
|
||||
import i18n from '~/locales'
|
||||
import { LANGUAGES } from '~/locales/languges'
|
||||
import {
|
||||
@ -19,6 +21,8 @@ import {
|
||||
MessageSquareDashed,
|
||||
MessageSquareText,
|
||||
NotebookPen,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -49,6 +53,7 @@ import md from '~/store/example.md?raw'
|
||||
|
||||
import { NoteIconType } from '../components/Dashboard/components/utils'
|
||||
import CustomDatabaseLocationSettings from './CustomDatabaseLocationSettings'
|
||||
import GlobalTemplatesSettings from './GlobalTemplatesSettings'
|
||||
|
||||
export default function UserPreferences() {
|
||||
const { t } = useTranslation()
|
||||
@ -336,9 +341,9 @@ export default function UserPreferences() {
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* ------------- Custom Database Location Settings Card ------------- */}
|
||||
<CustomDatabaseLocationSettings />
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
|
||||
<Spacer h={6} />
|
||||
|
||||
<Box className="animate-in fade-in max-w-xl mt-4">
|
||||
<Card>
|
||||
@ -872,17 +877,42 @@ export default function UserPreferences() {
|
||||
{t('How to set hotkeys:', { ns: 'settings2' })}
|
||||
</Text>
|
||||
<ul className="text-xs text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<li>• {t('Click Set/Change button to start recording', { ns: 'settings2' })}</li>
|
||||
<li>• {t('Press your desired key combination (e.g., Ctrl+Shift+V)', { ns: 'settings2' })}</li>
|
||||
<li>• {t('Press Enter to confirm or Escape to cancel', { ns: 'settings2' })}</li>
|
||||
<li>• {t('Press Backspace/Delete to clear the hotkey', { ns: 'settings2' })}</li>
|
||||
<li>
|
||||
•{' '}
|
||||
{t('Click Set/Change button to start recording', {
|
||||
ns: 'settings2',
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
•{' '}
|
||||
{t(
|
||||
'Press your desired key combination (e.g., Ctrl+Shift+V)',
|
||||
{ ns: 'settings2' }
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
•{' '}
|
||||
{t('Press Enter to confirm or Escape to cancel', {
|
||||
ns: 'settings2',
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
•{' '}
|
||||
{t('Press Backspace/Delete to clear the hotkey', {
|
||||
ns: 'settings2',
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Box className="mb-4">
|
||||
<div className="relative">
|
||||
<InputField
|
||||
label={t('Show/Hide Main App Window', { ns: 'settings2' })}
|
||||
value={isEditingMainApp ? (currentKeyPreview || mainAppHotkey) : mainAppHotkey}
|
||||
value={
|
||||
isEditingMainApp
|
||||
? currentKeyPreview || mainAppHotkey
|
||||
: mainAppHotkey
|
||||
}
|
||||
autoFocus={isEditingMainApp}
|
||||
disabled={!isEditingMainApp}
|
||||
onKeyDown={e =>
|
||||
@ -895,7 +925,11 @@ export default function UserPreferences() {
|
||||
? t('Press your key combination...', { ns: 'settings2' })
|
||||
: mainAppHotkey || t('No keys set', { ns: 'settings2' })
|
||||
}
|
||||
className={`${isEditingMainApp ? 'border-blue-300 dark:border-blue-600' : ''}`}
|
||||
className={`${
|
||||
isEditingMainApp
|
||||
? 'border-blue-300 dark:border-blue-600'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
{isEditingMainApp && (
|
||||
<div className="absolute right-3 top-8 text-xs text-blue-600 dark:text-blue-400">
|
||||
@ -952,7 +986,11 @@ export default function UserPreferences() {
|
||||
<div className="relative">
|
||||
<InputField
|
||||
label={t('Show/Hide Quick Paste Window', { ns: 'settings2' })}
|
||||
value={isEditingQuickPaste ? (currentKeyPreview || quickPasteHotkey) : quickPasteHotkey}
|
||||
value={
|
||||
isEditingQuickPaste
|
||||
? currentKeyPreview || quickPasteHotkey
|
||||
: quickPasteHotkey
|
||||
}
|
||||
disabled={!isEditingQuickPaste}
|
||||
autoFocus={isEditingQuickPaste}
|
||||
onKeyDown={e =>
|
||||
@ -963,9 +1001,14 @@ export default function UserPreferences() {
|
||||
placeholder={
|
||||
isEditingQuickPaste
|
||||
? t('Press your key combination...', { ns: 'settings2' })
|
||||
: quickPasteHotkey || t('No keys set', { ns: 'settings2' })
|
||||
: quickPasteHotkey ||
|
||||
t('No keys set', { ns: 'settings2' })
|
||||
}
|
||||
className={`${isEditingQuickPaste ? 'border-blue-300 dark:border-blue-600' : ''}`}
|
||||
className={`${
|
||||
isEditingQuickPaste
|
||||
? 'border-blue-300 dark:border-blue-600'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
{isEditingQuickPaste && (
|
||||
<div className="absolute right-3 top-8 text-xs text-blue-600 dark:text-blue-400">
|
||||
@ -1507,7 +1550,7 @@ export default function UserPreferences() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<GlobalTemplatesSettings />
|
||||
<Spacer h={6} />
|
||||
<Link to={returnRoute} replace>
|
||||
<Button
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { emit, listen, TauriEvent } from '@tauri-apps/api/event'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
import { relaunch } from '@tauri-apps/api/process'
|
||||
import { checkUpdate, installUpdate } from '@tauri-apps/api/updater'
|
||||
import { semverCompare } from '~/libs/utils'
|
||||
@ -105,6 +105,8 @@ type Settings = {
|
||||
isKeepStarredOnClearEnabled: boolean
|
||||
hasPinProtectedCollections: boolean
|
||||
protectedCollections: string[]
|
||||
globalTemplatesEnabled: boolean
|
||||
globalTemplates: Array<{ id: string; name: string; value: string; isEnabled: boolean }>
|
||||
}
|
||||
|
||||
type Constants = {
|
||||
@ -215,6 +217,16 @@ export interface SettingsStoreState {
|
||||
setClipTextMaxLength: (height: number) => void
|
||||
setProtectedCollections: (ids: string[]) => void
|
||||
setHasPinProtectedCollections: (hasPinProtectedCollections: boolean) => Promise<void>
|
||||
setGlobalTemplatesEnabled: (isEnabled: boolean) => void
|
||||
addGlobalTemplate: (template: { name: string; value: string }) => void
|
||||
updateGlobalTemplate: (template: {
|
||||
id: string
|
||||
name?: string
|
||||
value?: string
|
||||
isEnabled?: boolean
|
||||
}) => void
|
||||
deleteGlobalTemplate: (templateId: string) => void
|
||||
toggleGlobalTemplateEnabledState: (templateId: string) => void
|
||||
}
|
||||
|
||||
const initialState: SettingsStoreState & Settings = {
|
||||
@ -296,6 +308,8 @@ const initialState: SettingsStoreState & Settings = {
|
||||
isKeepStarredOnClearEnabled: false,
|
||||
protectedCollections: [],
|
||||
hasPinProtectedCollections: false,
|
||||
globalTemplatesEnabled: true,
|
||||
globalTemplates: [],
|
||||
setHasPinProtectedCollections: async () => {},
|
||||
CONST: {
|
||||
APP_DETECT_LANGUAGES_SUPPORTED: [],
|
||||
@ -409,6 +423,11 @@ const initialState: SettingsStoreState & Settings = {
|
||||
verifyPassword: (password: string, hash: string): Promise<boolean> =>
|
||||
invoke('verify_password', { password, hash }),
|
||||
setProtectedCollections: () => {},
|
||||
setGlobalTemplatesEnabled: () => {},
|
||||
addGlobalTemplate: () => {},
|
||||
updateGlobalTemplate: () => {},
|
||||
deleteGlobalTemplate: () => {},
|
||||
toggleGlobalTemplateEnabledState: () => {},
|
||||
}
|
||||
|
||||
export const settingsStore = createStore<SettingsStoreState & Settings>()((set, get) => ({
|
||||
@ -477,6 +496,18 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
|
||||
}))
|
||||
}
|
||||
|
||||
if (name === 'globalTemplates' && typeof value === 'string') {
|
||||
try {
|
||||
const parsedTemplates = JSON.parse(value)
|
||||
return set(() => ({
|
||||
globalTemplates: Array.isArray(parsedTemplates) ? parsedTemplates : [],
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error('Failed to parse globalTemplates from settings:', e)
|
||||
return set(() => ({ globalTemplates: [] })) // Fallback to empty array on parse error
|
||||
}
|
||||
}
|
||||
|
||||
return set(() => ({ [name]: value }))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@ -767,6 +798,64 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
|
||||
get().syncStateUpdate('isKeepStarredOnClearEnabled', isEnabled)
|
||||
return get().updateSetting('isKeepStarredOnClearEnabled', isEnabled)
|
||||
},
|
||||
setGlobalTemplatesEnabled: async (isEnabled: boolean) => {
|
||||
get().syncStateUpdate('globalTemplatesEnabled', isEnabled)
|
||||
return get().updateSetting('globalTemplatesEnabled', isEnabled)
|
||||
},
|
||||
addGlobalTemplate: async (template: { name: string; value: string }) => {
|
||||
const newTemplate = {
|
||||
id: Date.now().toString(),
|
||||
name: template.name,
|
||||
value: template.value,
|
||||
isEnabled: true,
|
||||
}
|
||||
const currentTemplates = get().globalTemplates || []
|
||||
const updatedTemplates = [...currentTemplates, newTemplate]
|
||||
get().syncStateUpdate('globalTemplates', updatedTemplates)
|
||||
|
||||
// Do not persist templates with empty name or value
|
||||
if (newTemplate.name.trim() === '' || newTemplate.value.trim() === '') {
|
||||
return
|
||||
}
|
||||
|
||||
return get().updateSetting('globalTemplates', JSON.stringify(updatedTemplates))
|
||||
},
|
||||
updateGlobalTemplate: async (templateToUpdate: {
|
||||
id: string
|
||||
name?: string
|
||||
value?: string
|
||||
isEnabled?: boolean
|
||||
}) => {
|
||||
const currentTemplates = get().globalTemplates || []
|
||||
|
||||
const updatedTemplates = currentTemplates.map(t =>
|
||||
t.id === templateToUpdate.id
|
||||
? {
|
||||
...t,
|
||||
...templateToUpdate,
|
||||
...(typeof templateToUpdate.name === 'string' && {
|
||||
name: templateToUpdate.name.trim(),
|
||||
}),
|
||||
}
|
||||
: t
|
||||
)
|
||||
|
||||
return get().updateSetting('globalTemplates', JSON.stringify(updatedTemplates))
|
||||
},
|
||||
deleteGlobalTemplate: async (templateId: string) => {
|
||||
const currentTemplates = get().globalTemplates || []
|
||||
const updatedTemplates = currentTemplates.filter(t => t.id !== templateId)
|
||||
get().syncStateUpdate('globalTemplates', updatedTemplates)
|
||||
return get().updateSetting('globalTemplates', JSON.stringify(updatedTemplates))
|
||||
},
|
||||
toggleGlobalTemplateEnabledState: async (templateId: string) => {
|
||||
const currentTemplates = get().globalTemplates || []
|
||||
const updatedTemplates = currentTemplates.map(t =>
|
||||
t.id === templateId ? { ...t, isEnabled: !t.isEnabled } : t
|
||||
)
|
||||
get().syncStateUpdate('globalTemplates', updatedTemplates)
|
||||
return get().updateSetting('globalTemplates', JSON.stringify(updatedTemplates))
|
||||
},
|
||||
setProtectedCollections: async (ids: string[]) => {
|
||||
return get().updateSetting('protectedCollections', ids.join(','))
|
||||
},
|
||||
@ -1018,6 +1107,26 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
|
||||
{} as Settings
|
||||
)
|
||||
|
||||
// Parse globalTemplates if it's a string from loaded settings
|
||||
if (
|
||||
newInitSettings.globalTemplates &&
|
||||
typeof newInitSettings.globalTemplates === 'string'
|
||||
) {
|
||||
try {
|
||||
const parsed = JSON.parse(newInitSettings.globalTemplates as string) // Explicit cast
|
||||
newInitSettings.globalTemplates = Array.isArray(parsed) ? parsed : []
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Error parsing globalTemplates from storage during initSettings:',
|
||||
e
|
||||
)
|
||||
newInitSettings.globalTemplates = [] // Fallback to empty array
|
||||
}
|
||||
} else if (!Array.isArray(newInitSettings.globalTemplates)) {
|
||||
// Ensure it's an array if it's not a string (e.g. null, undefined, or already an object but not array)
|
||||
newInitSettings.globalTemplates = []
|
||||
}
|
||||
|
||||
set(prev => ({
|
||||
...prev,
|
||||
...newInitSettings,
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::models::models::UpdatedItemData;
|
||||
use crate::services::history_service;
|
||||
|
||||
use crate::models::Setting;
|
||||
use crate::services::items_service::update_item_by_id;
|
||||
use crate::services::request_service::{
|
||||
run_web_request, run_web_scraping, HttpRequest, HttpScraping,
|
||||
@ -9,7 +10,8 @@ use crate::services::shell_service::{
|
||||
run_shell_command, ExecHomeDir, OutputRegexFilter, OutputTemplate,
|
||||
};
|
||||
use crate::services::utils::{
|
||||
ensure_url_or_email_prefix, ensure_url_prefix, mask_value, remove_special_bbcode_tags,
|
||||
apply_global_templates, ensure_url_or_email_prefix, ensure_url_prefix, mask_value,
|
||||
remove_special_bbcode_tags,
|
||||
};
|
||||
use crate::{constants, services::items_service::get_item_by_id};
|
||||
use arboard::{Clipboard, ImageData};
|
||||
@ -19,6 +21,7 @@ use inputbot::KeybdKey::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::{thread, time::Duration};
|
||||
use tauri::{self, AppHandle};
|
||||
use tauri::{ClipboardManager, Manager};
|
||||
@ -76,6 +79,7 @@ pub struct TemplateOption {
|
||||
pub is_value_masked: Option<bool>,
|
||||
pub select_options: Option<Vec<String>>,
|
||||
pub is_enable: Option<bool>,
|
||||
pub is_global: Option<bool>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -140,7 +144,7 @@ pub fn copy_history_item(app_handle: AppHandle, history_id: String) -> String {
|
||||
IMAGE_NOT_FOUND_BASE64.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
None => IMAGE_NOT_FOUND_BASE64.to_string(),
|
||||
};
|
||||
|
||||
@ -234,14 +238,19 @@ pub async fn copy_clip_item(
|
||||
match all_options {
|
||||
Ok(options) => {
|
||||
match run_template_fill(
|
||||
app_handle,
|
||||
app_handle.clone(),
|
||||
item.value.clone(),
|
||||
options.template_options,
|
||||
None,
|
||||
) {
|
||||
Ok(filled_template) => {
|
||||
// Apply global templates after local template processing
|
||||
let app_settings = app_handle.state::<Mutex<HashMap<String, Setting>>>();
|
||||
let settings_map = app_settings.lock().unwrap();
|
||||
let final_text = apply_global_templates(&filled_template, &settings_map);
|
||||
|
||||
manager
|
||||
.write_text(&filled_template)
|
||||
.write_text(&final_text)
|
||||
.expect("Failed to write to clipboard");
|
||||
|
||||
"ok".to_string()
|
||||
@ -426,7 +435,7 @@ pub async fn copy_clip_item(
|
||||
IMAGE_NOT_FOUND_BASE64.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
None => IMAGE_NOT_FOUND_BASE64.to_string(),
|
||||
};
|
||||
|
||||
@ -444,7 +453,12 @@ pub async fn copy_clip_item(
|
||||
remove_special_bbcode_tags(&text)
|
||||
};
|
||||
|
||||
match manager.write_text(clean_text) {
|
||||
// Apply global templates
|
||||
let app_settings = app_handle.state::<Mutex<HashMap<String, Setting>>>();
|
||||
let settings_map = app_settings.lock().unwrap();
|
||||
let final_text = apply_global_templates(&clean_text, &settings_map);
|
||||
|
||||
match manager.write_text(final_text) {
|
||||
Ok(_) => "ok".to_string(),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to write to clipboard: {}", e);
|
||||
@ -620,12 +634,51 @@ pub fn run_template_fill(
|
||||
return Ok("".to_string());
|
||||
}
|
||||
|
||||
// Get global templates from settings for use in the loop
|
||||
let app_settings = app_handle.state::<Mutex<HashMap<String, Setting>>>();
|
||||
let settings_map = app_settings.lock().unwrap();
|
||||
|
||||
// Get global templates from settings
|
||||
let global_templates_enabled = settings_map
|
||||
.get("globalTemplatesEnabled")
|
||||
.and_then(|s| s.value_bool)
|
||||
.unwrap_or(false);
|
||||
|
||||
let global_templates_json = settings_map
|
||||
.get("globalTemplates")
|
||||
.and_then(|s| s.value_text.as_ref())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "[]".to_string());
|
||||
|
||||
let global_templates: Vec<serde_json::Value> = serde_json::from_str(&global_templates_json)
|
||||
.unwrap_or_else(|_| Vec::new());
|
||||
|
||||
for field in template_options
|
||||
.iter()
|
||||
.filter(|f| f.is_enable.unwrap_or(false))
|
||||
{
|
||||
if let Some(true) = field.is_enable {
|
||||
if let Some(value) = &field.value {
|
||||
let field_value = if field.is_global.unwrap_or(false) && global_templates_enabled {
|
||||
// For global templates, fetch the current value from global templates
|
||||
let field_label = field.label.as_deref().unwrap_or_default();
|
||||
global_templates
|
||||
.iter()
|
||||
.find(|gt| {
|
||||
gt.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|n| n.eq_ignore_ascii_case(field_label))
|
||||
.unwrap_or(false)
|
||||
&& gt.get("isEnabled").and_then(|e| e.as_bool()).unwrap_or(false)
|
||||
})
|
||||
.and_then(|gt| gt.get("value"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| field.value.clone())
|
||||
} else {
|
||||
field.value.clone()
|
||||
};
|
||||
|
||||
if let Some(value) = field_value {
|
||||
let regex = regex::Regex::new(&format!(
|
||||
r"(?i)\{{\{{\s*{}\s*\}}\}}",
|
||||
regex::escape(field.label.as_deref().unwrap_or_default())
|
||||
@ -659,12 +712,16 @@ pub fn run_template_fill(
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any unmatched template placeholders
|
||||
replaced_template = regex::Regex::new(r"(?i)\{\{\s*[^}]*\s*\}\}")
|
||||
.unwrap()
|
||||
.replace_all(&replaced_template, "")
|
||||
.to_string();
|
||||
|
||||
Ok(replaced_template)
|
||||
// Apply remaining global templates that weren't handled as local fields
|
||||
let final_text = apply_global_templates(&replaced_template, &settings_map);
|
||||
|
||||
Ok(final_text)
|
||||
}
|
||||
|
||||
pub fn paste_clipboard(delay: i32) -> String {
|
||||
|
@ -50,7 +50,7 @@ use crate::models::Setting;
|
||||
use crate::services::history_service;
|
||||
use crate::services::settings_service::get_all_settings;
|
||||
use crate::services::translations::translations::Translations;
|
||||
use crate::services::utils::ensure_url_or_email_prefix;
|
||||
use crate::services::utils::{apply_global_templates, ensure_url_or_email_prefix};
|
||||
use crate::services::utils::remove_special_bbcode_tags;
|
||||
use commands::backup_restore_commands;
|
||||
use commands::clipboard_commands;
|
||||
@ -767,11 +767,13 @@ async fn main() {
|
||||
let url = item.value.as_deref().unwrap_or("");
|
||||
if is_copy_only {
|
||||
// Copy URL to clipboard instead of opening it
|
||||
// Apply global templates
|
||||
let final_text = apply_global_templates(url, &settings_map);
|
||||
debug_output(|| {
|
||||
println!("Copying URL to clipboard: {}", url);
|
||||
println!("Copying URL to clipboard: {}", final_text);
|
||||
});
|
||||
manager
|
||||
.write_text(url)
|
||||
.write_text(final_text)
|
||||
.expect("failed to write to clipboard");
|
||||
} else {
|
||||
let _ = opener::open(ensure_url_or_email_prefix(url))
|
||||
@ -781,30 +783,36 @@ async fn main() {
|
||||
let path = item.value.as_deref().unwrap_or("");
|
||||
if is_copy_only {
|
||||
// Copy path to clipboard instead of opening it
|
||||
// Apply global templates
|
||||
let final_text = apply_global_templates(path, &settings_map);
|
||||
debug_output(|| {
|
||||
println!("Copying path to clipboard: {}", path);
|
||||
println!("Copying path to clipboard: {}", final_text);
|
||||
});
|
||||
manager
|
||||
.write_text(path)
|
||||
.write_text(final_text)
|
||||
.expect("failed to write to clipboard");
|
||||
} else {
|
||||
let _ = opener::open(path).map_err(|e| format!("Failed to open path: {}", e));
|
||||
}
|
||||
} else {
|
||||
if item.value.as_deref().unwrap_or("").is_empty() {
|
||||
// Apply global templates to item name
|
||||
let final_text = apply_global_templates(&item.name, &settings_map);
|
||||
debug_output(|| {
|
||||
println!("Copying item name to clipboard: {}", &item.name);
|
||||
println!("Copying item name to clipboard: {}", final_text);
|
||||
});
|
||||
manager
|
||||
.write_text(&item.name)
|
||||
.write_text(final_text)
|
||||
.expect("failed to write to clipboard");
|
||||
} else if let Some(ref item_value) = item.value {
|
||||
let text_to_copy = remove_special_bbcode_tags(item_value);
|
||||
// Apply global templates
|
||||
let final_text = apply_global_templates(&text_to_copy, &settings_map);
|
||||
debug_output(|| {
|
||||
println!("Copying item value to clipboard: {}", text_to_copy);
|
||||
println!("Copying item value to clipboard: {}", final_text);
|
||||
});
|
||||
manager
|
||||
.write_text(text_to_copy)
|
||||
.write_text(final_text)
|
||||
.expect("failed to write to clipboard");
|
||||
}
|
||||
}
|
||||
@ -905,8 +913,10 @@ async fn main() {
|
||||
Some(val) => val,
|
||||
None => return (),
|
||||
};
|
||||
// Apply global templates
|
||||
let final_text = apply_global_templates(&value, &settings_map);
|
||||
manager
|
||||
.write_text(value)
|
||||
.write_text(final_text)
|
||||
.expect("failed to write to clipboard");
|
||||
}
|
||||
|
||||
@ -945,6 +955,12 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
},
|
||||
#[cfg(target_os = "windows")]
|
||||
SystemTrayEvent::DoubleClick { .. } => {
|
||||
let window = app.get_window("main").unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.on_window_event(|event| {
|
||||
|
@ -4,8 +4,10 @@ use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use winreg::RegKey;
|
||||
@ -14,9 +16,17 @@ use tld;
|
||||
use url::Url;
|
||||
|
||||
use crate::menu::AssociatedItemTree;
|
||||
use crate::models::Setting;
|
||||
|
||||
use super::collections_service;
|
||||
|
||||
// Global regex cache for template patterns
|
||||
lazy_static! {
|
||||
static ref REGEX_CACHE: Mutex<HashMap<String, Regex>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub const GLOBAL_TEMPLATES_ENABLED_KEY: &str = "globalTemplatesEnabled";
|
||||
|
||||
pub fn pretty_print_json<T: Serialize>(data: &Result<T, diesel::result::Error>) -> String {
|
||||
data
|
||||
.as_ref()
|
||||
@ -207,6 +217,73 @@ pub fn is_valid_json(text: &str) -> bool {
|
||||
serde_json::from_str::<Value>(text).is_ok()
|
||||
}
|
||||
|
||||
pub fn apply_global_templates(text: &str, settings_map: &HashMap<String, Setting>) -> String {
|
||||
// Check if global templates are enabled
|
||||
let is_enabled = settings_map
|
||||
.get(GLOBAL_TEMPLATES_ENABLED_KEY)
|
||||
.and_then(|s| s.value_bool)
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_enabled {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
// Get global templates from settings
|
||||
let templates_json = match settings_map
|
||||
.get("globalTemplates")
|
||||
.and_then(|s| s.value_text.as_ref())
|
||||
{
|
||||
Some(json) => json,
|
||||
None => return text.to_string(),
|
||||
};
|
||||
|
||||
// Parse templates JSON
|
||||
let templates: Vec<serde_json::Value> = match serde_json::from_str(templates_json) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return text.to_string(),
|
||||
};
|
||||
|
||||
let mut result = text.to_string();
|
||||
|
||||
// Apply each enabled template
|
||||
for template in templates {
|
||||
let is_template_enabled = template
|
||||
.get("isEnabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_template_enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = match template.get("name").and_then(|v| v.as_str()) {
|
||||
Some(n) => n,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let value = match template.get("value").and_then(|v| v.as_str()) {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Check if regex is already cached
|
||||
let re = {
|
||||
let mut cache = REGEX_CACHE.lock().unwrap();
|
||||
cache
|
||||
.entry(name.to_string())
|
||||
.or_insert_with(|| {
|
||||
let pattern = format!(r"(?i)\{{\{{\s*{}\s*\}}\}}", regex::escape(name));
|
||||
Regex::new(&pattern).unwrap()
|
||||
})
|
||||
.clone()
|
||||
};
|
||||
|
||||
result = re.replace_all(&result, value).to_string();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn debug_output<F: FnOnce()>(f: F) {
|
||||
if cfg!(debug_assertions) {
|
||||
f();
|
||||
|
Loading…
x
Reference in New Issue
Block a user