Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/components/BatchExportFieldsGuide.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const closeAccordion = () => {
}

const simpleFields: Array<{
name: keyof SimpleCSVData
name: string
required: boolean
description: string
example: string
Expand All @@ -40,11 +40,17 @@ const simpleFields: Array<{
required: false,
description: 'Custom filename for the exported QR code',
example: 'my_custom_name'
},
{
name: 'frameFontFamily',
required: false,
description: 'Font family for the frame text (must be one of the predefined options)',
example: 'Poppins'
}
]

const vCardFields: Array<{
name: keyof VCardCSVData
name: string
required: boolean
description: string
example: string
Expand Down Expand Up @@ -75,17 +81,23 @@ const vCardFields: Array<{
required: false,
description: 'Custom filename for the exported QR code',
example: 'john_doe_contact'
},
{
name: 'frameFontFamily',
required: false,
description: 'Font family for the frame text (must be one of the predefined options)',
example: 'Roboto'
}
]

const simpleCsvExample = `url,frameText,fileName
https://example.com,Visit us,site
https://github.com/user,GitHub,github
https://linkedin.com,LinkedIn,linkedin`
const simpleCsvExample = `url,frameText,fileName,frameFontFamily
https://example.com,Visit us,site,Poppins
https://github.com/user,GitHub,github,Roboto
https://linkedin.com,LinkedIn,linkedin,`

const vCardCsvExample = `firstName,lastName,org,email,frameText
John,Doe,Acme,john@ex.com,Contact
Jane,Smith,Tech,jane@ex.com,Manager`
const vCardCsvExample = `firstName,lastName,org,email,frameText,frameFontFamily
John,Doe,Acme,john@ex.com,Contact,Roboto
Jane,Smith,Tech,jane@ex.com,Manager,`
</script>

<template>
Expand Down
79 changes: 73 additions & 6 deletions src/components/QRCodeCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,18 @@ import { getNumericCSSValue } from '@/utils/formatting'
import {
allFramePresets,
defaultFramePreset,
FONT_OPTIONS,
loadGoogleFont,
type FramePreset,
type FrameStyle
} from '@/utils/framePresets'
import { allQrCodePresets, defaultPreset, type Preset } from '@/utils/qrCodePresets'
import {
CUSTOM_LOADED_PRESET_KEYS,
LAST_LOADED_LOCALLY_PRESET_KEY,
LOADED_FROM_FILE_PRESET_KEY,
hasStoredQRConfig,
isLocalStorageEnabled,
LAST_LOADED_LOCALLY_PRESET_KEY,
LOADED_FROM_FILE_PRESET_KEY,
loadQRConfig,
saveQRConfig,
serializeQRConfig,
Expand Down Expand Up @@ -378,6 +380,17 @@ const frameSettings = computed(() => ({
position: frameTextPosition.value,
style: frameStyle.value
}))

function onFontFamilyChange(value: string): Promise<void> {
// value may be a label name (e.g. "Poppins" from CSV) or a full CSS value (e.g. "'Poppins', sans-serif" from UI)
const font = FONT_OPTIONS.find((f) => f.value === value || f.label === value)
const resolvedValue = font ? font.value : value
frameStyle.value = { ...frameStyle.value, fontFamily: resolvedValue || undefined }
if (font?.googleFontName) {
return loadGoogleFont(font.googleFontName)
}
return Promise.resolve()
}
//#endregion

//#region /* Frame text autofill */ Fill if empty */
Expand Down Expand Up @@ -539,7 +552,7 @@ function downloadQRImage(format: 'png' | 'svg' | 'jpg') {
el,
formatConfig.filename,
{ ...getExportDimensions(), ...formatConfig.extraOptions },
styledBorderRadiusFormatted.value
exportBorderRadius.value
)
} else {
generateBatchQRCodes(format)
Expand Down Expand Up @@ -587,6 +600,14 @@ function applyQRConfig(config: QRCodeConfig, key?: string) {
frameTextPosition.value = config.frame.position || 'bottom'
frameStyle.value = { ...frameStyle.value, ...config.frame.style }

const restoredFontFamily = config.frame.style.fontFamily
if (restoredFontFamily) {
const font = FONT_OPTIONS.find((f) => f.value === restoredFontFamily)
if (font?.googleFontName) {
loadGoogleFont(font.googleFontName)
}
}

const framePreset: FramePreset = {
name: key || LAST_LOADED_LOCALLY_PRESET_KEY,
style: config.frame.style,
Expand Down Expand Up @@ -672,6 +693,7 @@ const exportMode = ref(ExportMode.Single)
const dataStringsFromCsv = ref<string[]>([])
const frameTextsFromCsv = ref<string[]>([])
const fileNamesFromCsv = ref<string[]>([])
const fontFamiliesFromCsv = ref<string[]>([])

const inputFileForBatchEncoding = ref<File | null>(null)
const fileInput = ref<HTMLInputElement | null>(null)
Expand Down Expand Up @@ -709,6 +731,7 @@ const resetData = () => {
dataStringsFromCsv.value = []
frameTextsFromCsv.value = []
fileNamesFromCsv.value = []
fontFamiliesFromCsv.value = []
isValidCsv.value = true
resetBatchExportProgress()
isBatchExportSuccess.value = false
Expand All @@ -727,6 +750,10 @@ watch(previewRowIndex, (newIndex) => {
) {
data.value = dataStringsFromCsv.value[newIndex]
frameText.value = frameTextsFromCsv.value[newIndex] || defaultFrameText.value
const fontFamily = fontFamiliesFromCsv.value[newIndex]
if (fontFamily !== undefined) {
onFontFamilyChange(fontFamily)
}
}
})

Expand Down Expand Up @@ -778,6 +805,7 @@ const onBatchInputFileUpload = (event: Event) => {
dataStringsFromCsv.value = batchResult.urls
frameTextsFromCsv.value = batchResult.frameTexts
fileNamesFromCsv.value = batchResult.fileNames
fontFamiliesFromCsv.value = batchResult.fontFamilies
showFrame.value = batchResult.hasCustomFrameText
isValidCsv.value = true
previewRowIndex.value = 0 // Reset preview to first row on new upload
Expand All @@ -786,6 +814,10 @@ const onBatchInputFileUpload = (event: Event) => {
if (batchResult.urls.length > 0) {
data.value = batchResult.urls[0]
frameText.value = batchResult.frameTexts[0] || defaultFrameText.value
const firstFontFamily = batchResult.fontFamilies[0]
if (firstFontFamily) {
onFontFamilyChange(firstFontFamily)
}
}
}

Expand Down Expand Up @@ -837,22 +869,26 @@ async function generateBatchQRCodes(format: 'png' | 'svg' | 'jpg') {
currentExportedQrCodeIndex.value = index
const url = dataStringsFromCsv.value[index]
const currentFrameText = frameTextsFromCsv.value[index]
const currentFontFamily = fontFamiliesFromCsv.value[index]
data.value = url
frameText.value = currentFrameText
if (currentFontFamily !== undefined) {
await onFontFamilyChange(currentFontFamily)
}
await sleep(1000)
let dataUrl: string = ''
if (format === 'png') {
dataUrl = await getPngElement(el, getExportDimensions(), styledBorderRadiusFormatted.value)
dataUrl = await getPngElement(el, getExportDimensions(), exportBorderRadius.value)
} else if (format === 'jpg') {
const jpgBgcolor =
styleBackground.value === 'transparent' ? '#ffffff' : styleBackground.value
dataUrl = await getJpgElement(
el,
{ ...getExportDimensions(), bgcolor: jpgBgcolor },
styledBorderRadiusFormatted.value
exportBorderRadius.value
)
} else {
dataUrl = await getSvgString(el, getExportDimensions(), styledBorderRadiusFormatted.value)
dataUrl = await getSvgString(el, getExportDimensions(), exportBorderRadius.value)
}
createZipFile(zip, dataUrl, index, format)
numQrCodesCreated++
Expand Down Expand Up @@ -1425,6 +1461,26 @@ const updateDataFromModal = (newData: string) => {
placeholder="16px"
/>
</div>
<div class="sm:col-span-2">
<label for="frame-font-family" class="mb-1 block text-sm">{{
t('Font family')
}}</label>
<select
id="frame-font-family"
class="w-full text-input"
:value="frameStyle.fontFamily ?? ''"
@change="onFontFamilyChange(($event.target as HTMLSelectElement).value)"
>
<option
v-for="font in FONT_OPTIONS"
:key="font.value"
:value="font.value"
:style="font.value ? { fontFamily: font.value } : {}"
>
{{ font.label }}
</option>
</select>
</div>
</div>
</div>
</template>
Expand Down Expand Up @@ -1626,6 +1682,17 @@ const updateDataFromModal = (newData: string) => {
{{ fileNamesFromCsv[previewRowIndex] }}
</code>
</div>
<div v-if="fontFamiliesFromCsv[previewRowIndex]">
<span
class="text-xs font-medium text-gray-500 dark:text-gray-400"
>{{ $t('Font family') }}</span
>
<code
class="rounded bg-white px-2 py-1 font-mono text-sm dark:bg-gray-900"
>
{{ fontFamiliesFromCsv[previewRowIndex] }}
</code>
</div>
</div>
</div>
<div class="mt-2 flex items-center justify-between">
Expand Down
24 changes: 13 additions & 11 deletions src/components/QRCodeFrame.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface FrameStyle {
borderWidth?: string
borderRadius?: string
padding?: string
fontFamily?: string
}

interface Props {
Expand Down Expand Up @@ -44,16 +45,17 @@ withDefaults(defineProps<Props>(), {
}"
>
<slot name="qr-code"></slot>
<p
:style="{
color: frameStyle.textColor,
margin: 0,
textAlign: 'center',
[textPosition === 'left' || textPosition === 'right' ? 'width' : 'maxWidth']: '200px',
whiteSpace: 'pre-line'
}"
>
{{ frameText }}
</p>
<p
:style="{
color: frameStyle.textColor,
fontFamily: frameStyle.fontFamily || undefined,
margin: 0,
textAlign: 'center',
[textPosition === 'left' || textPosition === 'right' ? 'width' : 'maxWidth']: '200px',
whiteSpace: 'pre-line'
}"
>
{{ frameText }}
</p>
</div>
</template>
47 changes: 47 additions & 0 deletions src/utils/csv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,53 @@ https://docs.example.com/,,documentation`
})
})

it('parses simple CSV with frameFontFamily column correctly', () => {
const csvContent = `url,frameText,fileName,frameFontFamily
https://example.com,Visit us,site,Poppins
https://github.com,GitHub,github,Roboto
https://linkedin.com,LinkedIn,linkedin,`

const result = parseCSV(csvContent)
expect(result.isValid).toBe(true)
expect(result.data).toHaveLength(3)
expect(result.data[0]).toEqual({
url: 'https://example.com',
frameText: 'Visit us',
fileName: 'site',
frameFontFamily: 'Poppins'
})
expect(result.data[1]).toEqual({
url: 'https://github.com',
frameText: 'GitHub',
fileName: 'github',
frameFontFamily: 'Roboto'
})
expect(result.data[2]).toEqual({
url: 'https://linkedin.com',
frameText: 'LinkedIn',
fileName: 'linkedin',
frameFontFamily: undefined
})
})

it('parses vCard CSV with frameFontFamily column correctly', () => {
const csvContent = `firstname,lastname,email,frameText,frameFontFamily
John,Doe,john@example.com,Contact John,Poppins
Jane,Smith,jane@example.com,Contact Jane,`

const result = parseCSV(csvContent)
expect(result.isValid).toBe(true)
expect(result.data).toHaveLength(2)
expect(result.data[0]).toMatchObject({
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
frameText: 'Contact John',
frameFontFamily: 'Poppins'
})
expect((result.data[1] as any).frameFontFamily).toBeUndefined()
})

it('handles empty CSV content', () => {
const result = parseCSV('')
expect(result.isValid).toBe(false)
Expand Down
13 changes: 12 additions & 1 deletion src/utils/csv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface SimpleCSVData {
url: string
frameText?: string
fileName?: string
frameFontFamily?: string
}

export type VCardOptionalFields =
Expand All @@ -24,6 +25,7 @@ export interface VCardCSVData extends Partial<Record<VCardOptionalFields, string
firstName: string
lastName: string
fileName?: string
frameFontFamily?: string
}

export type CSVData = SimpleCSVData | VCardCSVData
Expand Down Expand Up @@ -163,6 +165,12 @@ export const parseCSV = (csvContent: string): CSVParsingResult => {
vCardData.fileName = values[fileNameIndex] || ''
}

// Handle frameFontFamily separately (CSV column: frameFontFamily)
const frameFontFamilyIndex = headers.indexOf('framefontfamily')
if (frameFontFamilyIndex !== -1 && values[frameFontFamilyIndex]) {
vCardData.frameFontFamily = values[frameFontFamilyIndex]
}

data.push(vCardData as VCardCSVData)
}
} else {
Expand All @@ -187,7 +195,10 @@ export const parseCSV = (csvContent: string): CSVParsingResult => {
values.push(currentValue.trim().replace(/^["']|["']$/g, ''))

const [url, frameText, fileName] = values
data.push({ url, frameText, fileName })
const frameFontFamilyIndex = headers.indexOf('framefontfamily')
const frameFontFamily =
frameFontFamilyIndex >= 0 ? values[frameFontFamilyIndex] || undefined : undefined
data.push({ url, frameText, fileName, frameFontFamily })
}
}

Expand Down
Loading
Loading