Skip to content
Open
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
4 changes: 3 additions & 1 deletion core/frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ import blueos_blue from '@/assets/img/blueos-logo-blue.svg'
import blueos_white from '@/assets/img/blueos-logo-white.svg'
import consoleLogger from '@/libs/console-logger'
import settings from '@/libs/settings'
import customization_store from '@/store/customization'
import helper from '@/store/helper'
import wifi from '@/store/wifi'
import { Service } from '@/types/helper'
Expand Down Expand Up @@ -760,7 +761,7 @@ export default Vue.extend({
return import.meta.env.VITE_BUILD_DATE
},
blueos_logo(): string {
return settings.is_dark_theme ? blueos_white : blueos_blue
return customization_store.logoUrl ?? (settings.is_dark_theme ? blueos_white : blueos_blue)
},
is_cloud_tray_menu_visible(): boolean {
// Keep tray menu visible in everything except stable versions
Expand Down Expand Up @@ -795,6 +796,7 @@ export default Vue.extend({
this.setupCallbacks()
this.checkTour()
updateTime()
customization_store.refreshAll()

const body = document.querySelector('body')
body?.addEventListener('click', (event) => {
Expand Down
18 changes: 11 additions & 7 deletions core/frontend/src/components/app/VehicleBanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import Vue from 'vue'
import ardupilot_data from '@/store/autopilot'
import bag from '@/store/bag'
import beacon from '@/store/beacon'
import customization_store from '@/store/customization'

import ImagePicker from './ImagePicker.vue'

Expand All @@ -83,7 +84,7 @@ export default Vue.extend({
data() {
return {
vehicle_name_input: '',
vehicle_image: undefined as string | undefined,
bag_vehicle_image: undefined as string | undefined,
logo_image: null as string | null,
mdns_hostname_input: '',
dialog: false,
Expand All @@ -99,6 +100,9 @@ export default Vue.extend({
system_id() {
return ardupilot_data.system_id
},
vehicle_image(): string | undefined {
return customization_store.vehicleImageUrl ?? this.bag_vehicle_image
},
},
watch: {
vehicle_name() {
Expand All @@ -114,16 +118,16 @@ export default Vue.extend({
},
mounted() {
beacon.registerBeaconListener(this)
this.load_vehicle_image()
this.load_bag_vehicle_image()
this.vehicle_name_input = this.vehicle_name
this.mdns_hostname_input = this.mdns_hostname
this.load_company_logo()
},
methods: {
async load_vehicle_image() {
this.vehicle_image = (await bag.getData('vehicle.image_path'))?.url as (string | undefined)
if (this.vehicle_image && !this.vehicle_image.startsWith('/')) {
this.vehicle_image = `/${this.vehicle_image}`
async load_bag_vehicle_image() {
this.bag_vehicle_image = (await bag.getData('vehicle.image_path'))?.url as (string | undefined)
if (this.bag_vehicle_image && !this.bag_vehicle_image.startsWith('/')) {
this.bag_vehicle_image = `/${this.bag_vehicle_image}`
}
},
async load_company_logo() {
Expand All @@ -148,7 +152,7 @@ export default Vue.extend({
})
},
save_vehicle_image(image: string) {
this.vehicle_image = image
this.bag_vehicle_image = image
bag.setData('vehicle.image_path', {
url: image,
})
Expand Down
117 changes: 117 additions & 0 deletions core/frontend/src/components/customization/BrandingUploader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<template>
<v-card outlined class="pa-3">
<div class="d-flex align-center">
<v-avatar tile size="64" color="grey lighten-3" class="mr-3">
<v-img v-if="asset?.url" :src="asset.url" contain />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Branding previews ignore the cache-busting logic used elsewhere for logo/vehicle images.

The store’s brandingVersion is used to build cache-busted logoUrl / vehicleImageUrl with ?v=..., but this component bypasses that by using asset.url directly. When ThemeCustomization passes customization_store.logo / vehicleImage, the preview can remain stale after updates. Please either pass in the already cache-busted URLs (e.g. dedicated preview URL props) or change this component to take a plain src so callers can handle cache-busting themselves.

Suggested implementation:

      <v-avatar tile size="64" color="grey lighten-3" class="mr-3">
        <v-img v-if="previewUrl" :src="previewUrl" contain />

      <div class="flex-grow-1">
        <div v-if="previewUrl" class="text-body-2">
          <a :href="previewUrl" target="_blank" rel="noopener noreferrer">
            {{ previewUrl.split('/').pop() }}

To fully wire this up you should also:

  1. Expose a previewUrl prop (or equivalent) on this component

    • In the <script> block of BrandingUploader.vue, add a previewUrl: { type: String, default: '' } prop (or previewUrl?: string via defineProps if using <script setup>).
    • Keep the existing asset prop if it’s still used for other metadata; it just won’t be used for the preview URL anymore.
  2. Update all call sites of BrandingUploader

    • Wherever you currently pass an asset with .url that should be cache-busted (e.g. customization_store.logo, vehicleImage), construct the URL with branding versioning and pass it in:
      • Example: :preview-url="brandingStore.logoUrl" or :preview-url="${customization_store.logo.url}?v=${brandingVersion}" depending on your existing helpers.
    • If callers still need asset for other data, continue to pass it alongside preview-url.
  3. Optional: backward compatibility

    • If you want to avoid breaking existing call sites immediately, you can define a computed previewUrl that falls back to asset?.url when the previewUrl prop is not provided, then switch call sites gradually:
      const previewUrl = computed(() => props.previewUrl || props.asset?.url || '');
    • This computed should match the previewUrl used in the template above.

<v-icon v-else color="grey">
mdi-image-off-outline
</v-icon>
</v-avatar>
<div class="flex-grow-1">
<div v-if="asset?.url" class="text-body-2">
<a :href="asset.url" target="_blank" rel="noopener noreferrer">
{{ asset.url.split('/').pop() }}
</a>
</div>
<div v-else class="text-caption text--secondary">
{{ emptyLabel }}
</div>
<div v-if="asset?.size_bytes" class="text-caption text--secondary">
{{ formatSize(asset.size_bytes) }}
</div>
</div>
</div>
<div class="d-flex justify-end mt-2">
<v-btn
v-tooltip="'Upload a new file'"
outlined
small
color="primary"
:loading="uploading"
@click="trigger_picker"
>
<v-icon left small>
mdi-upload
</v-icon>
{{ uploadLabel }}
</v-btn>
<v-btn
v-if="asset?.url"
v-tooltip="'Remove the custom file'"
outlined
small
color="error"
class="ml-2"
@click="$emit('remove')"
>
<v-icon left small>
mdi-trash-can
</v-icon>
Remove
</v-btn>
</div>
<label class="d-none">
Upload file
<input
ref="file_input"
type="file"
:accept="accept"
@change="on_file_change"
>
</label>
</v-card>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue'

import { BrandingAsset } from '@/types/customization'
import { prettifySize } from '@/utils/helper_functions'

export default Vue.extend({
name: 'BrandingUploader',

props: {
asset: {
type: Object as PropType<BrandingAsset | null>,
default: null,
},
accept: {
type: String,
default: 'image/*',
},
uploading: {
type: Boolean,
default: false,
},
emptyLabel: {
type: String,
default: 'No file uploaded.',
},
uploadLabel: {
type: String,
default: 'Upload',
},
},

methods: {
formatSize(bytes: number): string {
return prettifySize(bytes / 1024)
},

trigger_picker(): void {
const input = this.$refs.file_input as HTMLInputElement | undefined
input?.click()
},

on_file_change(event: Event): void {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (file) {
this.$emit('upload', file)
}
input.value = ''
},
},
})
</script>
Loading
Loading