Merge branch 'main' into feat/history-preview-limit

This commit is contained in:
Sergey Kurdin 2025-06-20 16:30:54 -04:00
commit e2972ecdba
37 changed files with 1529 additions and 205 deletions

View File

@ -0,0 +1,5 @@
---
'pastebar-app-ui': patch
---
Added global templates in user preferences for saved clips

View File

@ -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({

View File

@ -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) {

View File

@ -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>[]>([])

View File

@ -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
})

View File

@ -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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template: Feld <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Deaktiviertes Feld <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Feld <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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

View 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

View File

@ -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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template: Add field <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Disabled field <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Field <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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

View 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

View File

@ -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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template: Añadir campo <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Se ha encontrado el campo deshabilitado <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Se ha encontrado el campo <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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

View File

@ -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

View File

@ -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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template: Ajouter le champ <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Le champ désactivé <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Le champ <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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

View 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

View File

@ -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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template: Aggiungi il campo <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Il campo disabilitato <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Il campo <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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

View 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

View File

@ -21,8 +21,7 @@ Add Section: Добавить раздел
Add Tab: Добавить вкладку
Add Template Field: Добавить поле шаблона
Add a Tab: Добавить вкладку
Add field <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template: Добавить поле <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Отключенное поле <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Поле <b>&#123;&#123;<b>{{name}}<b>&#123;&#123;<b> найдено в шаблоне
Field {{name}} has been found in the template: Поле {{name}} найдено в шаблоне
Field Options: Параметры поля
Fields Value: Значение полей
File, folder or app path does not exist: Путь к файлу, папке или приложению не существует

View 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: Значение

View File

@ -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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template: Şablona <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Etkisiz Alanlar <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Alan <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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

View 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

View File

@ -21,8 +21,7 @@ Add Section: Додати розділ
Add Tab: Додати вкладку
Add Template Field: Додати поле шаблону
Add a Tab: Додати вкладку
Add field <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template: Додати поле <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Вимкнене поле <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template: Поле <b>&#123;&#123;<b>{{name}}<b>&#123;&#123;<b> знайдено в шаблоні
Field {{name}} has been found in the template: Поле {{name}} знайдено в шаблоні
Field Options: Параметри поля
Fields Value: Значення полів
File, folder or app path does not exist: Шлях до файлу, папки або додатку не існує

View 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: Значення

View File

@ -21,8 +21,7 @@ Add Section: 添加部分
Add Tab: 添加标签
Add Template Field: 添加模板字段
Add a Tab: 添加一个标签
'Add field <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> into the template': '将字段 <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template': '在模板中找到禁用字段 <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b> has been found in the template': '在模板中找到字段 <b>&#123;&#123;<b>{{name}}</b>&#125;&#125;</b>'
Field {{name}} has been found in the template: 在模板中找到字段 {{name}}
Field Options: 字段选项
Fields Value: 字段值
File, folder or app path does not exist: 文件、文件夹或应用程序路径不存在

View File

@ -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:

View File

@ -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(() => {

View File

@ -463,14 +463,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) ||

View File

@ -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) {

View File

@ -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) {

View File

@ -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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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>&#123;&#123;<b>{{name}}</b>&#125;&#125;</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`}
>

View File

@ -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>
)

View File

@ -528,12 +528,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
}
}
@ -1109,13 +1111,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()
}
}}

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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

View File

@ -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'
@ -106,6 +106,8 @@ type Settings = {
isKeepStarredOnClearEnabled: boolean
hasPinProtectedCollections: boolean
protectedCollections: string[]
globalTemplatesEnabled: boolean
globalTemplates: Array<{ id: string; name: string; value: string; isEnabled: boolean }>
}
type Constants = {
@ -217,6 +219,16 @@ export interface SettingsStoreState {
setHistoryPreviewLineLimit: (limit: 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 = {
@ -299,6 +311,8 @@ const initialState: SettingsStoreState & Settings = {
isKeepStarredOnClearEnabled: false,
protectedCollections: [],
hasPinProtectedCollections: false,
globalTemplatesEnabled: true,
globalTemplates: [],
setHasPinProtectedCollections: async () => {},
CONST: {
APP_DETECT_LANGUAGES_SUPPORTED: [],
@ -413,6 +427,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) => ({
@ -481,6 +500,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)
@ -774,6 +805,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(','))
},
@ -1025,6 +1114,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,

View File

@ -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 {

View File

@ -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| {

View File

@ -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();