Skip to content

Commit 88d9d36

Browse files
fixed QR generation moved export to composition api improved qr quality
1 parent d204db0 commit 88d9d36

File tree

2 files changed

+92
-75
lines changed

2 files changed

+92
-75
lines changed

webapp/src/views/schedule/export-select.vue

Lines changed: 92 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
>
3030
<transition name="fade">
3131
<div
32-
v-if="hoveredOption === option"
32+
v-if="hoveredOption === option.id"
3333
class="qr-popup"
3434
>
3535
<img
@@ -43,82 +43,91 @@
4343
</div>
4444
</template>
4545

46-
<script>
46+
<script setup>
47+
import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue'
4748
import QRCode from 'qrcode'
4849
import config from 'config'
4950
50-
export default {
51-
props: {
52-
options: {
53-
type: Array,
54-
required: true
55-
},
56-
modelValue: {
57-
type: Object,
58-
default: null
59-
}
60-
},
61-
emits: ['input', 'update:modelValue'],
62-
data() {
63-
return {
64-
isOpen: false,
65-
selectedOption: this.modelValue ? this.modelValue.label : null,
66-
hoveredOption: null,
67-
qrCodes: {}
68-
}
69-
},
70-
watch: {
71-
modelValue(newVal) {
72-
this.selectedOption = newVal ? newVal.label : null;
73-
}
74-
},
75-
mounted() {
76-
document.addEventListener('click', this.outsideClick)
77-
},
78-
beforeUnmount() {
79-
document.removeEventListener('click', this.outsideClick)
80-
},
81-
created() {
82-
this.options.forEach(option => {
83-
this.generateQRCode(option)
84-
})
85-
},
86-
methods: {
87-
selectOption(option) {
88-
this.selectedOption = option.label
89-
this.isOpen = false
90-
this.$emit('update:modelValue', option)
91-
this.$emit('input', option)
92-
},
93-
outsideClick(event) {
94-
const dropdown = this.$refs.dropdown
95-
if (!dropdown.contains(event.target)) {
96-
this.isOpen = false
97-
}
98-
},
99-
generateQRCode(option) {
100-
if (!['ics', 'xml', 'myics', 'myxml'].includes(option.id)) {
101-
return
102-
}
103-
const url = config.api.base + 'export-talk?export_type=' + option.id
104-
QRCode.toDataURL(url, { scale: 1 }, (err, url) => {
105-
if (!err) this.qrCodes[option.id] = url
106-
})
107-
},
108-
setHoveredOption(option) {
109-
if (['ics', 'xml', 'myics', 'myxml'].includes(option.id)) {
110-
this.hoveredOption = option
111-
} else {
112-
this.hoveredOption = null
113-
}
114-
},
115-
clearHoveredOption(option) {
116-
if (this.hoveredOption === option) {
117-
this.hoveredOption = null
118-
}
119-
},
120-
}
51+
const props = defineProps({
52+
options: {
53+
type: Array,
54+
required: true
55+
},
56+
modelValue: {
57+
type: Object,
58+
default: null
59+
}
60+
})
61+
62+
const emit = defineEmits(['input', 'update:modelValue'])
63+
64+
const dropdown = ref(null)
65+
const isOpen = ref(false)
66+
const selectedOption = ref(props.modelValue ? props.modelValue.label : null)
67+
const hoveredOption = ref(null)
68+
const qrCodes = reactive({})
69+
70+
watch(() => props.modelValue, (newVal) => {
71+
selectedOption.value = newVal ? newVal.label : null
72+
})
73+
74+
watch(() => props.options, (newOpts) => {
75+
// regenerate QR codes when options change
76+
for (const k in qrCodes) delete qrCodes[k]
77+
if (Array.isArray(newOpts)) {
78+
newOpts.forEach(option => generateQRCode(option))
79+
}
80+
}, { immediate: true, deep: true })
81+
82+
function selectOption(option) {
83+
selectedOption.value = option.label
84+
isOpen.value = false
85+
emit('update:modelValue', option)
86+
emit('input', option)
12187
}
88+
89+
function outsideClick(event) {
90+
const dd = dropdown.value
91+
if (dd && !dd.contains(event.target)) {
92+
isOpen.value = false
93+
}
94+
}
95+
96+
async function generateQRCode(option) {
97+
if (!['ics', 'xml', 'myics', 'myxml'].includes(option.id)) return
98+
const url = config.api.base + 'export-talk?export_type=' + option.id
99+
// generate a larger image to avoid pixelation when scaled down
100+
const popupSize = 150 // CSS popup size in px
101+
const ratio = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1
102+
const targetWidth = Math.ceil(popupSize * Math.max(1, ratio))
103+
try {
104+
const dataUrl = await QRCode.toDataURL(url, { width: targetWidth, margin: 1, errorCorrectionLevel: 'H' })
105+
qrCodes[option.id] = dataUrl
106+
} catch (err) {
107+
// Keep behavior silent on error (same as previous implementation).
108+
// Optional: console.error('QR generation failed', err)
109+
}
110+
}
111+
112+
function setHoveredOption(option) {
113+
if (['ics', 'xml', 'myics', 'myxml'].includes(option.id)) {
114+
hoveredOption.value = option.id
115+
} else {
116+
hoveredOption.value = null
117+
}
118+
}
119+
120+
function clearHoveredOption(option) {
121+
if (hoveredOption.value === option.id) hoveredOption.value = null
122+
}
123+
124+
onMounted(() => {
125+
document.addEventListener('click', outsideClick)
126+
})
127+
128+
onBeforeUnmount(() => {
129+
document.removeEventListener('click', outsideClick)
130+
})
122131
</script>
123132

124133
<style>
@@ -194,4 +203,14 @@ export default {
194203
height: 20px; /* Adjust as needed */
195204
align-self: flex-end;
196205
}
206+
207+
/* Ensure QR images keep sharp edges and are not blurry when displayed */
208+
.dropdown-options img.default-image,
209+
.qr-popup img {
210+
display: block;
211+
object-fit: contain;
212+
image-rendering: -webkit-optimize-contrast; /* Safari */
213+
image-rendering: crisp-edges;
214+
image-rendering: pixelated; /* fallback */
215+
}
197216
</style>

webapp/src/views/schedule/index.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,13 @@
2020
bunt-select.timezone-item(name="timezone", :options="[{id: schedule.timezone, label: schedule.timezone}, {id: userTimezone, label: userTimezone}]", v-model="currentTimezone", @blur="saveTimezone")
2121
template(v-else)
2222
div.timezone-label.timezone-item.bunt-tab-header-item {{ schedule.timezone }}
23-
2423
.export.dropdown
2524
bunt-progress-circular.export-spinner(v-if="isExporting", size="small")
2625
custom-dropdown(name="calendar-add1"
2726
:modelValue="selectedExporter"
2827
@update:modelValue="selectedExporter = $event; makeExport()"
2928
:options="exportType"
3029
label="Add to Calendar")
31-
3230
bunt-tabs.days(v-if="days && days.length > 1", :active-tab="currentDay.toISOString()", ref="tabs", v-scrollbar.x="")
3331
bunt-tab(v-for="day in days", :key="day.toISOString()", :id="day.toISOString()", :header="moment(day).format('dddd DD. MMMM')", @selected="changeDay(day)")
3432
.scroll-parent(ref="scrollParent", v-scrollbar.x.y="")

0 commit comments

Comments
 (0)