From 8d8db13a0aafbc3f238d032fa9ceb2431158dbf5 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Thu, 27 Aug 2026 18:45:09 -0400 Subject: [PATCH 01/14] add customUI section --- .../Attributes/AttributeCustomUI.vue | 128 +++++++++ .../components/Attributes/AttributeEditor.vue | 49 +++- .../Attributes/AttributeShortcuts.vue | 2 +- .../CustomUI/ButtonShortcutEditor.vue | 4 +- .../components/CustomUI/CustomUIBase.vue | 246 ++++++++++++++---- client/src/use/AttributeTypes.ts | 11 + client/src/use/attributeCustomUI.ts | 104 ++++++++ server/dive_utils/models.py | 10 + 8 files changed, 485 insertions(+), 69 deletions(-) create mode 100644 client/dive-common/components/Attributes/AttributeCustomUI.vue create mode 100644 client/src/use/attributeCustomUI.ts diff --git a/client/dive-common/components/Attributes/AttributeCustomUI.vue b/client/dive-common/components/Attributes/AttributeCustomUI.vue new file mode 100644 index 00000000..f750b37a --- /dev/null +++ b/client/dive-common/components/Attributes/AttributeCustomUI.vue @@ -0,0 +1,128 @@ + + + diff --git a/client/dive-common/components/Attributes/AttributeEditor.vue b/client/dive-common/components/Attributes/AttributeEditor.vue index 1459108d..4d00248a 100644 --- a/client/dive-common/components/Attributes/AttributeEditor.vue +++ b/client/dive-common/components/Attributes/AttributeEditor.vue @@ -3,12 +3,19 @@ import { computed, defineComponent, PropType, Ref, ref, watch, } from 'vue'; import { - Attribute, AttributeShortcut, MetadataLinkOptions, NumericAttributeEditorOptions, StringAttributeEditorOptions, + Attribute, AttributeCustomUI, AttributeShortcut, MetadataLinkOptions, NumericAttributeEditorOptions, StringAttributeEditorOptions, } from 'vue-media-annotator/use/AttributeTypes'; +import { + buildCustomUIPayload, + resolvedCustomUIToEditorValue, + resolveAttributeCustomUI, + stripLegacyDisplayValueFromShortcuts, +} from 'vue-media-annotator/use/attributeCustomUI'; import { usePrompt } from 'dive-common/vue-utilities/prompt-service'; import { useTrackStyleManager } from 'vue-media-annotator/provides'; import AttributeShortcuts from './AttributeShortcuts.vue'; import AttributeRendering from './AttributeRendering.vue'; +import AttributeCustomUIEditor from './AttributeCustomUI.vue'; import AttributeValueColors from './AttributeValueColors.vue'; import AttributeNumberValueColors from './AttributeNumberColors.vue'; import AttributeMetadataLink from './AttributeMetadataLink.vue'; @@ -18,6 +25,7 @@ export default defineComponent({ components: { AttributeShortcuts, AttributeRendering, + AttributeCustomUIEditor, AttributeValueColors, AttributeNumberValueColors, AttributeMetadataLink, @@ -84,10 +92,19 @@ export default defineComponent({ metadataLinkFromAttribute(props.selectedAttribute), ); + const customUIFromAttribute = (attr: Attribute): AttributeCustomUI => ( + resolvedCustomUIToEditorValue(resolveAttributeCustomUI(attr)) + ); + + const customUI: Ref = ref( + customUIFromAttribute(props.selectedAttribute), + ); + watch( () => props.selectedAttribute.key, () => { metadataLink.value = metadataLinkFromAttribute(props.selectedAttribute); + customUI.value = customUIFromAttribute(props.selectedAttribute); }, ); let values: string[] = props.selectedAttribute.values ? props.selectedAttribute.values : []; @@ -121,6 +138,7 @@ export default defineComponent({ metadataLink.value = { key: '', updateValue: false, useDynamicKeyFromAttribute: false, dynamicKeyAttributeKey: undefined, }; + customUI.value = customUIFromAttribute({ shortcuts: [] }); } function add() { setDefaultValue(); @@ -146,11 +164,13 @@ export default defineComponent({ key: `${belongs.value}_${name.value}`, editor: editor.value, color: color.value ? color.value : tempColor.value, - shortcuts: shortcuts.value, + shortcuts: stripLegacyDisplayValueFromShortcuts(shortcuts.value), user: user.value ? true : undefined, render: renderingVals.value, lockedValues: lockedValues.value, }; + const customUIPayload = buildCustomUIPayload(customUI.value); + data.customUI = customUIPayload; if (valueOrder) { data.valueOrder = valueOrder; } @@ -329,6 +349,7 @@ export default defineComponent({ launchColorEditor, saveAttributeValueColors, metadataLink, + customUI, }; }, }); @@ -343,11 +364,12 @@ export default defineComponent({ Main Shortcuts + Custom UI Rendering - MetadataLink Value Colors + MetadataLink @@ -539,6 +561,9 @@ export default defineComponent({ :attribute-color="color || tempColor" /> + + + - - - + + + diff --git a/client/dive-common/components/Attributes/AttributeShortcuts.vue b/client/dive-common/components/Attributes/AttributeShortcuts.vue index 03978b69..1c5c033a 100644 --- a/client/dive-common/components/Attributes/AttributeShortcuts.vue +++ b/client/dive-common/components/Attributes/AttributeShortcuts.vue @@ -474,7 +474,7 @@ export default defineComponent({ diff --git a/client/dive-common/components/CustomUI/ButtonShortcutEditor.vue b/client/dive-common/components/CustomUI/ButtonShortcutEditor.vue index 35d3ce2e..7ce39798 100644 --- a/client/dive-common/components/CustomUI/ButtonShortcutEditor.vue +++ b/client/dive-common/components/CustomUI/ButtonShortcutEditor.vue @@ -76,7 +76,7 @@ export default defineComponent({ const applyTypeDefaults = () => { syncingFromProps = true; - const { displayValue, buttonToolTip } = buttonShortcut.value; + const { buttonToolTip } = buttonShortcut.value; buttonShortcut.value = { ...defaultButtonForType( props.shortcutType, @@ -84,7 +84,6 @@ export default defineComponent({ props.attributeColor, ), ...(buttonToolTip !== undefined ? { buttonToolTip } : {}), - ...(displayValue !== undefined ? { displayValue } : {}), }; syncingFromProps = false; updateButtonShortcut(); @@ -188,7 +187,6 @@ export default defineComponent({ - diff --git a/client/dive-common/components/CustomUI/CustomUIBase.vue b/client/dive-common/components/CustomUI/CustomUIBase.vue index 2f72bcfd..74455c6e 100644 --- a/client/dive-common/components/CustomUI/CustomUIBase.vue +++ b/client/dive-common/components/CustomUI/CustomUIBase.vue @@ -14,6 +14,12 @@ import AttributeSubsection from 'dive-common/components/Attributes/AttributesSub import { useStore } from 'platform/web-girder/store/types'; import { usePrompt } from 'dive-common/vue-utilities/prompt-service'; import { Attribute, AttributeShortcut } from 'vue-media-annotator/use/AttributeTypes'; +import { + formatAttributeDisplayValue, + LONG_VALUE_EXPAND_THRESHOLD, + ResolvedAttributeCustomUI, + resolveAttributeCustomUI, +} from 'vue-media-annotator/use/attributeCustomUI'; import { DIVEAction, DIVEMetadataAction } from 'dive-common/use/useActions'; import useMetadataLinkUpdater from 'dive-common/use/useMetadataLinkUpdater'; import type { MetadataLinkUpdateContext } from 'dive-common/use/useMetadataLinkUpdater'; @@ -26,7 +32,6 @@ interface AttributeDisplayButton { prependIcon?: string; appendIcon?: string; buttonToolTip?: string; - displayValue?: boolean; attrName: string; type: Attribute['belongs']; userAttribute: boolean; @@ -51,6 +56,7 @@ interface AttributeButtons { type: 'track' | 'detection'; description?: string; buttons: AttributeDisplayButton[]; + customUI: ResolvedAttributeCustomUI; } type AttributeButtonList = AttributeButtons[]; @@ -404,9 +410,46 @@ export default defineComponent({ return handler; }; + const getAttributeDisplayValue = ( + attribute: Attribute, + ): string | boolean | number | unknown => { + const hasSegmentShortcut = attribute.shortcuts?.some((shortcut) => shortcut.segment); + if (hasSegmentShortcut && selectedTrackIdRef.value !== null && frameRef.value !== undefined) { + const track = cameraStore.getAnyTrack(selectedTrackIdRef.value); + const rangeVals = track.getFrameAttributeRanges( + [attribute.name], + store.state.User.user?.login || null, + ); + const ranges = rangeVals[attribute.name]; + if (ranges && ranges.length > 0) { + for (let i = 0; i < ranges.length; i += 2) { + const start = ranges[i]; + const end = ranges[i + 1]; + if (frameRef.value >= start && frameRef.value <= end) { + const [real] = track.getFeature(start); + if (real?.attributes) { + if (attribute.user && real.attributes.userAttributes) { + const user = store.state.User.user?.login; + if (user && real.attributes.userAttributes[user]) { + return (real.attributes.userAttributes[user] as StringKeyObject)[attribute.name]; + } + } + return real.attributes[attribute.name]; + } + } + } + } + } + return getAttributeValue(attribute.name, attribute.belongs, !!attribute.user); + }; + const attributeButtons = computed(() => { const attributeButtonList: AttributeButtonList = []; attributes.value.forEach((attribute) => { + const customUI = resolveAttributeCustomUI(attribute); + if (!customUI.enabled) { + return; + } if (attribute.shortcuts && attribute.shortcuts.length > 0) { const buttons: AttributeDisplayButton[] = []; attribute.shortcuts.forEach((shortcut) => { @@ -418,7 +461,6 @@ export default defineComponent({ prependIcon: shortcut.button.iconPrepend, appendIcon: shortcut.button.iconAppend, buttonToolTip: tooltip, - displayValue: shortcut.button.displayValue, attrName: attribute.name, type: attribute.belongs, userAttribute: !!attribute.user, @@ -436,6 +478,7 @@ export default defineComponent({ description: attribute.description, type: attribute.belongs, buttons, + customUI, }); } } @@ -568,49 +611,42 @@ export default defineComponent({ // return buttonMapping; // }); - const buttonValueMap: Ref> = ref({}); + const buttonValueMap: Ref> = ref({}); const updateButtonMap = () => { - const buttonMapping: Record = {}; - attributeButtons.value.forEach((attribute) => attribute.buttons.forEach((button) => { - if (button.displayValue) { - if (button.segment && selectedTrackIdRef.value !== null) { - const track = cameraStore.getAnyTrack(selectedTrackIdRef.value); - const rangeVals = track.getFrameAttributeRanges([attribute.attrName], store.state.User.user?.login || null); - const ranges = rangeVals[attribute.attrName]; - if (ranges && ranges.length > 0) { - for (let i = 0; i < ranges.length; i += 2) { - const start = ranges[i]; - const end = ranges[i + 1]; - if (frameRef.value >= start && frameRef.value <= end) { - const [real] = track.getFeature(start); - if (real && real.attributes) { - if (button.userAttribute && real.attributes.userAttributes) { - const user = store.state.User.user?.login; - if (user && real.attributes.userAttributes[user]) { - const val = ((real.attributes.userAttributes[user] as StringKeyObject)[button.attrName] as string | boolean | number); - buttonMapping[button.attrName] = { - attribute: attribute.name, button: button.attrName, value: val, length: val ? (val as string | boolean | number).toString()?.length : 0, - }; - } - } else if (real.attributes) { - const val = (real.attributes[button.attrName] as string | boolean | number); - buttonMapping[button.attrName] = { - attribute: attribute.name, button: button.attrName, value: val, length: val ? (val as string | boolean | number).toString()?.length : 0, - }; - } - } - } - } - } - } else { - const val = getAttributeValue(button.attrName, button.type, button.userAttribute); - buttonMapping[button.attrName] = { - attribute: attribute.name, button: button.attrName, value: val, length: val ? (val as string | boolean | number).toString()?.length : 0, - }; - } + const buttonMapping: Record = {}; + attributeButtons.value.forEach((attributeGroup) => { + if (!attributeGroup.customUI.displayValue) { + return; } - })); + const attribute = attributes.value.find( + (item) => item.name === attributeGroup.attrName && item.belongs === attributeGroup.type, + ); + if (!attribute) { + return; + } + const rawValue = getAttributeDisplayValue(attribute); + const displayText = formatAttributeDisplayValue( + rawValue, + attributeGroup.customUI.emptyValueLabel, + ); + buttonMapping[attributeGroup.attrName] = { + attribute: attributeGroup.name, + value: displayText, + rawLength: displayText.length, + longValueMode: attributeGroup.customUI.longValueMode, + }; + }); buttonValueMap.value = buttonMapping; }; @@ -640,6 +676,7 @@ export default defineComponent({ panelExpanded, expandPanel, getButtonDisabled, + LONG_VALUE_EXPAND_THRESHOLD, }; }, }); @@ -692,12 +729,80 @@ export default defineComponent({

{{ attribute.name }}

+ + + + {{ buttonValueMap[attribute.attrName].value }} + + + {{ + buttonValueMap[attribute.attrName].longValueMode === 'truncate' + && buttonValueMap[attribute.attrName].rawLength >= LONG_VALUE_EXPAND_THRESHOLD + ? `${buttonValueMap[attribute.attrName].value.slice(0, LONG_VALUE_EXPAND_THRESHOLD)}...` + : buttonValueMap[attribute.attrName].value + }} + +
- +

{{ attribute.description }}

+ + + + + {{ buttonValueMap[attribute.attrName].value }} + + + {{ + buttonValueMap[attribute.attrName].longValueMode === 'truncate' + && buttonValueMap[attribute.attrName].rawLength >= LONG_VALUE_EXPAND_THRESHOLD + ? `${buttonValueMap[attribute.attrName].value.slice(0, LONG_VALUE_EXPAND_THRESHOLD)}...` + : buttonValueMap[attribute.attrName].value + }} + + + @@ -722,19 +827,39 @@ export default defineComponent({ - + - + + {{ buttonValueMap[attribute.attrName].value }} - - - {{ attribute.name }} Value - - {{ buttonValueMap[attribute.attrName].value }} - - - + + {{ + buttonValueMap[attribute.attrName].longValueMode === 'truncate' + && buttonValueMap[attribute.attrName].rawLength >= LONG_VALUE_EXPAND_THRESHOLD + ? `${buttonValueMap[attribute.attrName].value.slice(0, LONG_VALUE_EXPAND_THRESHOLD)}...` + : buttonValueMap[attribute.attrName].value + }} + @@ -742,3 +867,18 @@ export default defineComponent({ + + diff --git a/client/src/use/AttributeTypes.ts b/client/src/use/AttributeTypes.ts index b1675922..39614bbb 100644 --- a/client/src/use/AttributeTypes.ts +++ b/client/src/use/AttributeTypes.ts @@ -58,8 +58,18 @@ export interface ButtonShortcut { iconAppend?: string; iconPrepend?: string; buttonColor?: string; // 'auto' or can be overridden + /** @deprecated Use attribute customUI.displayValue. Kept for legacy configs. */ displayValue?: boolean; } + +export interface AttributeCustomUI { + enabled?: boolean; + displayValue?: boolean; + valuePosition?: 'below' | 'above' | 'header'; + longValueMode?: 'truncate' | 'expand' | 'scroll'; + emptyValueLabel?: string; + showDescription?: boolean; + } export interface AttributeShortcut { key?: string; type: 'set' | 'dialog' | 'remove'; @@ -155,6 +165,7 @@ export interface Attribute { lockedValues?: boolean; editor?: NumericAttributeEditorOptions | StringAttributeEditorOptions; shortcuts?: AttributeShortcut[]; + customUI?: AttributeCustomUI; render?: AttributeRendering; colorKey?: boolean; colorKeySettings?: {display: 'static' | 'selected'; trackFilter: string[] }; diff --git a/client/src/use/attributeCustomUI.ts b/client/src/use/attributeCustomUI.ts new file mode 100644 index 00000000..b0394551 --- /dev/null +++ b/client/src/use/attributeCustomUI.ts @@ -0,0 +1,104 @@ +import { Attribute, AttributeCustomUI, AttributeShortcut } from './AttributeTypes'; + +export const LONG_VALUE_EXPAND_THRESHOLD = 50; + +export interface ResolvedAttributeCustomUI { + enabled: boolean; + displayValue: boolean; + valuePosition: NonNullable; + longValueMode: NonNullable; + emptyValueLabel?: string; + showDescription: boolean; +} + +export function hadLegacyDisplayValue(shortcuts?: AttributeShortcut[]): boolean { + return !!shortcuts?.some((shortcut) => shortcut.button?.displayValue); +} + +export function resolveAttributeCustomUI( + attribute: Pick, +): ResolvedAttributeCustomUI { + const legacyDisplayValue = hadLegacyDisplayValue(attribute.shortcuts); + const { customUI } = attribute; + return { + enabled: customUI?.enabled ?? true, + displayValue: customUI?.displayValue ?? legacyDisplayValue ?? false, + valuePosition: customUI?.valuePosition ?? 'below', + longValueMode: customUI?.longValueMode ?? 'expand', + emptyValueLabel: customUI?.emptyValueLabel, + showDescription: customUI?.showDescription ?? true, + }; +} + +export function resolvedCustomUIToEditorValue( + resolved: ResolvedAttributeCustomUI, +): AttributeCustomUI { + const value: AttributeCustomUI = { + enabled: resolved.enabled, + displayValue: resolved.displayValue, + valuePosition: resolved.valuePosition, + longValueMode: resolved.longValueMode, + showDescription: resolved.showDescription, + }; + if (resolved.emptyValueLabel) { + value.emptyValueLabel = resolved.emptyValueLabel; + } + return value; +} + +export function buildCustomUIPayload( + customUI: AttributeCustomUI, +): AttributeCustomUI | undefined { + const payload: AttributeCustomUI = {}; + if (customUI.enabled === false) { + payload.enabled = false; + } + if (customUI.displayValue) { + payload.displayValue = true; + } + if (customUI.valuePosition && customUI.valuePosition !== 'below') { + payload.valuePosition = customUI.valuePosition; + } + if (customUI.longValueMode && customUI.longValueMode !== 'expand') { + payload.longValueMode = customUI.longValueMode; + } + if (customUI.emptyValueLabel?.length) { + payload.emptyValueLabel = customUI.emptyValueLabel; + } + if (customUI.showDescription === false) { + payload.showDescription = false; + } + return Object.keys(payload).length ? payload : undefined; +} + +export function stripLegacyDisplayValueFromShortcuts( + shortcuts: AttributeShortcut[] | undefined, +): AttributeShortcut[] | undefined { + if (!shortcuts?.length) { + return shortcuts; + } + let changed = false; + const cleaned = shortcuts.map((shortcut) => { + if (shortcut.button?.displayValue === undefined) { + return shortcut; + } + changed = true; + const { displayValue, ...button } = shortcut.button; + return { ...shortcut, button }; + }); + return changed ? cleaned : shortcuts; +} + +export function isEmptyAttributeValue(value: unknown): boolean { + return value === undefined || value === null || value === ''; +} + +export function formatAttributeDisplayValue( + value: unknown, + emptyValueLabel?: string, +): string { + if (isEmptyAttributeValue(value)) { + return emptyValueLabel ?? ''; + } + return String(value); +} diff --git a/server/dive_utils/models.py b/server/dive_utils/models.py index 49f8fa9a..5e277b00 100644 --- a/server/dive_utils/models.py +++ b/server/dive_utils/models.py @@ -136,7 +136,16 @@ class ButtonShortcut(BaseModel): iconAppend: Optional[str] iconPrepend: Optional[str] buttonColor: Optional[str] + displayValue: Optional[bool] # deprecated: use AttributeCustomUI.displayValue + + +class AttributeCustomUI(BaseModel): + enabled: Optional[bool] displayValue: Optional[bool] + valuePosition: Optional[Literal['below', 'above', 'header']] + longValueMode: Optional[Literal['truncate', 'expand', 'scroll']] + emptyValueLabel: Optional[str] + showDescription: Optional[bool] class ShortcutAttributeOptions(BaseModel): @@ -241,6 +250,7 @@ class Attribute(BaseModel): valueOrder: Optional[Dict[str, int]] displayText: Optional[str] metadataLink: Optional[MetadataLinkSettings] + customUI: Optional[AttributeCustomUI] class AttributeNumberFilter(BaseModel): From 4576683ccab9114ce71fe15ae03620eb85847c88 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Thu, 27 Aug 2026 18:59:50 -0400 Subject: [PATCH 02/14] show value even without buttons --- .../Attributes/AttributeCustomUI.vue | 15 ++++++++-- .../components/CustomUI/CustomUIBase.vue | 30 +++++++++---------- .../UISettings/UIContextBar.vue | 6 +++- client/src/use/AttributeTypes.ts | 2 ++ client/src/use/attributeCustomUI.ts | 17 +++++++++++ server/dive_utils/models.py | 1 + 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/client/dive-common/components/Attributes/AttributeCustomUI.vue b/client/dive-common/components/Attributes/AttributeCustomUI.vue index f750b37a..b57e2036 100644 --- a/client/dive-common/components/Attributes/AttributeCustomUI.vue +++ b/client/dive-common/components/Attributes/AttributeCustomUI.vue @@ -14,6 +14,7 @@ export default defineComponent({ }, setup(props, { emit }) { const enabled = ref(props.value.enabled ?? true); + const showWithoutButtons = ref(props.value.showWithoutButtons ?? false); const displayValue = ref(props.value.displayValue ?? false); const valuePosition = ref(props.value.valuePosition ?? 'below'); const longValueMode = ref(props.value.longValueMode ?? 'expand'); @@ -35,6 +36,7 @@ export default defineComponent({ const emitValue = () => { emit('input', { enabled: enabled.value, + showWithoutButtons: showWithoutButtons.value, displayValue: displayValue.value, valuePosition: valuePosition.value, longValueMode: longValueMode.value, @@ -47,6 +49,7 @@ export default defineComponent({ () => props.value, (newValue) => { enabled.value = newValue.enabled ?? true; + showWithoutButtons.value = newValue.showWithoutButtons ?? false; displayValue.value = newValue.displayValue ?? false; valuePosition.value = newValue.valuePosition ?? 'below'; longValueMode.value = newValue.longValueMode ?? 'expand'; @@ -57,12 +60,13 @@ export default defineComponent({ ); watch( - [enabled, displayValue, valuePosition, longValueMode, emptyValueLabel, showDescription], + [enabled, showWithoutButtons, displayValue, valuePosition, longValueMode, emptyValueLabel, showDescription], emitValue, ); return { enabled, + showWithoutButtons, displayValue, valuePosition, longValueMode, @@ -80,7 +84,14 @@ export default defineComponent({ + diff --git a/client/dive-common/components/CustomUI/CustomUIBase.vue b/client/dive-common/components/CustomUI/CustomUIBase.vue index 74455c6e..0dee8de6 100644 --- a/client/dive-common/components/CustomUI/CustomUIBase.vue +++ b/client/dive-common/components/CustomUI/CustomUIBase.vue @@ -19,6 +19,7 @@ import { LONG_VALUE_EXPAND_THRESHOLD, ResolvedAttributeCustomUI, resolveAttributeCustomUI, + shouldShowAttributeInCustomUI, } from 'vue-media-annotator/use/attributeCustomUI'; import { DIVEAction, DIVEMetadataAction } from 'dive-common/use/useActions'; import useMetadataLinkUpdater from 'dive-common/use/useMetadataLinkUpdater'; @@ -447,11 +448,8 @@ export default defineComponent({ const attributeButtonList: AttributeButtonList = []; attributes.value.forEach((attribute) => { const customUI = resolveAttributeCustomUI(attribute); - if (!customUI.enabled) { - return; - } - if (attribute.shortcuts && attribute.shortcuts.length > 0) { - const buttons: AttributeDisplayButton[] = []; + const buttons: AttributeDisplayButton[] = []; + if (attribute.shortcuts?.length) { attribute.shortcuts.forEach((shortcut) => { if (shortcut.button) { const { disabled, tooltip } = getButtonDisabled(attribute, shortcut); @@ -471,16 +469,16 @@ export default defineComponent({ }); } }); - if (buttons.length > 0) { - attributeButtonList.push({ - name: attribute.displayText || attribute.name, - attrName: attribute.name, - description: attribute.description, - type: attribute.belongs, - buttons, - customUI, - }); - } + } + if (shouldShowAttributeInCustomUI(attribute, buttons.length)) { + attributeButtonList.push({ + name: attribute.displayText || attribute.name, + attrName: attribute.name, + description: attribute.description, + type: attribute.belongs, + buttons, + customUI, + }); } }); const order = configMan.configuration.value?.customUI?.attributeButtonOrder || []; @@ -803,7 +801,7 @@ export default defineComponent({
- + diff --git a/client/dive-common/components/CustomUI/CustomUIBase.vue b/client/dive-common/components/CustomUI/CustomUIBase.vue index b54b37cd..49293a9d 100644 --- a/client/dive-common/components/CustomUI/CustomUIBase.vue +++ b/client/dive-common/components/CustomUI/CustomUIBase.vue @@ -641,6 +641,9 @@ export default defineComponent({ inherited: boolean; indicatorStyle: Record; tooltip: string; + valuePrepend?: string; + valueAppend?: string; + valueFontSizeScale: number; }>> = ref({}); const updateButtonMap = () => { @@ -652,6 +655,9 @@ export default defineComponent({ inherited: boolean; indicatorStyle: Record; tooltip: string; + valuePrepend?: string; + valueAppend?: string; + valueFontSizeScale: number; }> = {}; attributeButtons.value.forEach((attributeGroup) => { if (!attributeGroup.customUI.displayValue) { @@ -680,6 +686,9 @@ export default defineComponent({ attribute.color, ), tooltip: getStickyValueTooltip(inherited, displayText), + valuePrepend: attributeGroup.customUI.valuePrepend, + valueAppend: attributeGroup.customUI.valueAppend, + valueFontSizeScale: attributeGroup.customUI.valueFontSizeScale, }; }); buttonValueMap.value = buttonMapping; @@ -759,81 +768,93 @@ export default defineComponent({

Attribute Buttons

- + - - -

{{ attribute.name }}

- - - -
-
- -

{{ attribute.description }}

-
-
- - +

+ {{ attribute.name }} +

+
- - - - - - - {{ button.buttonToolTip }} - - - - - +
+

+ {{ attribute.description }} +

+
- - +
+ + + + + {{ button.buttonToolTip }} + + + +
+ +
+
@@ -841,8 +862,26 @@ export default defineComponent({ diff --git a/client/src/use/AttributeTypes.ts b/client/src/use/AttributeTypes.ts index b8bb4435..8402053b 100644 --- a/client/src/use/AttributeTypes.ts +++ b/client/src/use/AttributeTypes.ts @@ -85,6 +85,14 @@ export interface AttributeCustomUI { valuePosition?: 'below' | 'above' | 'header'; longValueMode?: 'truncate' | 'expand' | 'scroll'; emptyValueLabel?: string; + /** Text shown before the displayed attribute value. */ + valuePrepend?: string; + /** Text shown after the displayed attribute value. */ + valueAppend?: string; + /** Show the attribute name heading in the Custom UI panel. */ + showHeader?: boolean; + /** Font size multiplier for the displayed attribute value (1 = default). */ + valueFontSizeScale?: number; showDescription?: boolean; } export interface AttributeShortcut { diff --git a/client/src/use/attributeCustomUI.ts b/client/src/use/attributeCustomUI.ts index 111fc7be..e7e0442d 100644 --- a/client/src/use/attributeCustomUI.ts +++ b/client/src/use/attributeCustomUI.ts @@ -20,6 +20,10 @@ export interface ResolvedAttributeCustomUI { valuePosition: NonNullable; longValueMode: NonNullable; emptyValueLabel?: string; + valuePrepend?: string; + valueAppend?: string; + showHeader: boolean; + valueFontSizeScale: number; showDescription: boolean; } @@ -41,8 +45,8 @@ function resolveStickyValueIndicator( indicator?: AttributeCustomUIStickyIndicator, ): ResolvedAttributeCustomUIStickyIndicator { return { - bold: indicator?.bold ?? false, - italic: indicator?.italic ?? true, + bold: indicator?.bold ?? true, + italic: indicator?.italic ?? false, underline: indicator?.underline ?? false, highlightColor: indicator?.highlightColor, fontSizeScale: indicator?.fontSizeScale ?? 1, @@ -64,6 +68,10 @@ export function resolveAttributeCustomUI( valuePosition: customUI?.valuePosition ?? 'below', longValueMode: customUI?.longValueMode ?? 'expand', emptyValueLabel: customUI?.emptyValueLabel, + valuePrepend: customUI?.valuePrepend, + valueAppend: customUI?.valueAppend, + showHeader: customUI?.showHeader ?? true, + valueFontSizeScale: customUI?.valueFontSizeScale ?? 1, showDescription: customUI?.showDescription ?? true, }; } @@ -78,11 +86,25 @@ export function resolvedCustomUIToEditorValue( stickyValue: resolved.stickyValue, valuePosition: resolved.valuePosition, longValueMode: resolved.longValueMode, + showHeader: resolved.showHeader, + valueFontSizeScale: resolved.valueFontSizeScale, showDescription: resolved.showDescription, }; if (resolved.emptyValueLabel) { value.emptyValueLabel = resolved.emptyValueLabel; } + if (resolved.valuePrepend) { + value.valuePrepend = resolved.valuePrepend; + } + if (resolved.valueAppend) { + value.valueAppend = resolved.valueAppend; + } + if (resolved.valueFontSizeScale !== 1) { + value.valueFontSizeScale = resolved.valueFontSizeScale; + } + if (resolved.showHeader === false) { + value.showHeader = false; + } if (resolved.stickyValue) { value.stickyValueIndicator = { ...resolved.stickyValueIndicator }; } @@ -104,8 +126,8 @@ function stickyIndicatorDiffersFromDefault( indicator: AttributeCustomUIStickyIndicator, ): boolean { return !!( - indicator.bold - || indicator.italic === false + indicator.bold === false + || indicator.italic || indicator.underline || indicator.highlightColor || (indicator.fontSizeScale !== undefined && indicator.fontSizeScale !== 1) @@ -140,6 +162,18 @@ export function buildCustomUIPayload( if (customUI.emptyValueLabel?.length) { payload.emptyValueLabel = customUI.emptyValueLabel; } + if (customUI.valuePrepend?.length) { + payload.valuePrepend = customUI.valuePrepend; + } + if (customUI.valueAppend?.length) { + payload.valueAppend = customUI.valueAppend; + } + if (customUI.valueFontSizeScale !== undefined && customUI.valueFontSizeScale !== 1) { + payload.valueFontSizeScale = customUI.valueFontSizeScale; + } + } + if (customUI.showHeader === false) { + payload.showHeader = false; } if (customUI.showDescription === false) { payload.showDescription = false; @@ -233,6 +267,17 @@ export function resolveStickyAttributeValue( return { value: options.currentValue, inherited: false }; } +export function getCustomUIDisplayValueFontSizeStyle( + fontSizeScale: number, +): Record { + if (fontSizeScale === 1) { + return {}; + } + return { + fontSize: `${Math.round(fontSizeScale * 100)}%`, + }; +} + export function getStickyValueIndicatorStyle( indicator: ResolvedAttributeCustomUIStickyIndicator, inherited: boolean, diff --git a/server/dive_utils/models.py b/server/dive_utils/models.py index 58de0ac7..19490d05 100644 --- a/server/dive_utils/models.py +++ b/server/dive_utils/models.py @@ -157,6 +157,10 @@ class AttributeCustomUI(BaseModel): valuePosition: Optional[Literal['below', 'above', 'header']] longValueMode: Optional[Literal['truncate', 'expand', 'scroll']] emptyValueLabel: Optional[str] + valuePrepend: Optional[str] + valueAppend: Optional[str] + showHeader: Optional[bool] + valueFontSizeScale: Optional[float] showDescription: Optional[bool] From 1bd23c135fb673cf972f0179bc760eb56b03007c Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Thu, 27 Aug 2026 20:15:32 -0400 Subject: [PATCH 05/14] more attribute display options --- .../Attributes/AttributeCustomUI.vue | 252 ++++++++++++++++-- .../components/Attributes/AttributeEditor.vue | 2 +- .../CustomUIAttributeValueDisplay.vue | 20 +- .../components/CustomUI/CustomUIBase.vue | 18 +- client/src/use/AttributeTypes.ts | 4 + client/src/use/attributeCustomUI.ts | 61 ++++- client/src/use/attributeValueColor.ts | 32 +++ client/src/use/useAttributes.ts | 27 +- server/dive_utils/models.py | 2 + 9 files changed, 363 insertions(+), 55 deletions(-) create mode 100644 client/src/use/attributeValueColor.ts diff --git a/client/dive-common/components/Attributes/AttributeCustomUI.vue b/client/dive-common/components/Attributes/AttributeCustomUI.vue index 225f6a1c..557b4871 100644 --- a/client/dive-common/components/Attributes/AttributeCustomUI.vue +++ b/client/dive-common/components/Attributes/AttributeCustomUI.vue @@ -2,7 +2,9 @@ import { computed, defineComponent, PropType, ref, watch, nextTick, } from 'vue'; -import { AttributeCustomUI, AttributeCustomUIStickyIndicator } from 'vue-media-annotator/use/AttributeTypes'; +import { AttributeCustomUI, AttributeCustomUIStickyIndicator, Attribute } from 'vue-media-annotator/use/AttributeTypes'; +import { createGetAttributeValueColor } from 'vue-media-annotator/use/attributeValueColor'; +import { useTrackStyleManager } from 'vue-media-annotator/provides'; const defaultStickyIndicator = (): AttributeCustomUIStickyIndicator => ({ bold: true, @@ -26,6 +28,8 @@ function buildCustomUIEditorPayload( valueAppend: string, showHeader: boolean, valueFontSizeScale: number, + valueAlign: AttributeCustomUI['valueAlign'], + valueColor: string | undefined, showDescription: boolean, ): AttributeCustomUI { const payload: AttributeCustomUI = { @@ -45,6 +49,12 @@ function buildCustomUIEditorPayload( if (valueFontSizeScale !== 1) { payload.valueFontSizeScale = valueFontSizeScale; } + if (valueAlign && valueAlign !== 'left') { + payload.valueAlign = valueAlign; + } + if (valueColor) { + payload.valueColor = valueColor; + } if (stickyValue) { payload.stickyValueIndicator = { ...stickyIndicator }; } @@ -63,8 +73,13 @@ export default defineComponent({ type: Object as PropType, required: true, }, + attribute: { + type: Object as PropType, + default: undefined, + }, }, setup(props, { emit }) { + const getAttributeValueColor = createGetAttributeValueColor(useTrackStyleManager()); const enabled = ref(props.value.enabled ?? true); const showWithoutButtons = ref(props.value.showWithoutButtons ?? false); const displayValue = ref(props.value.displayValue ?? false); @@ -81,6 +96,15 @@ export default defineComponent({ const valueAppend = ref(props.value.valueAppend ?? ''); const showHeader = ref(props.value.showHeader ?? true); const valueFontSizeScale = ref(props.value.valueFontSizeScale ?? 1); + const valueAlign = ref(props.value.valueAlign ?? 'left'); + const valueColor = ref(props.value.valueColor); + const valueColorCustom = ref( + props.value.valueColor && props.value.valueColor !== 'auto' + ? props.value.valueColor + : '#FFFFFF', + ); + const editingValueColor = ref(false); + const editingHighlightColor = ref(false); const showDescription = ref(props.value.showDescription ?? true); let syncingFromProps = false; @@ -96,6 +120,12 @@ export default defineComponent({ { text: 'Scroll', value: 'scroll' }, ]; + const valueAlignOptions = [ + { text: 'Left', value: 'left' }, + { text: 'Center', value: 'center' }, + { text: 'Right', value: 'right' }, + ]; + const fontSizeScaleOptions = [ { text: 'Same size', value: 1 }, { text: 'Smaller', value: 0.85 }, @@ -119,6 +149,8 @@ export default defineComponent({ valueAppend.value, showHeader.value, valueFontSizeScale.value, + valueAlign.value, + valueColor.value, showDescription.value, ); if (customUIPayloadsEqual(payload, props.value)) { @@ -145,6 +177,11 @@ export default defineComponent({ valueAppend.value = newValue.valueAppend ?? ''; showHeader.value = newValue.showHeader ?? true; valueFontSizeScale.value = newValue.valueFontSizeScale ?? 1; + valueAlign.value = newValue.valueAlign ?? 'left'; + valueColor.value = newValue.valueColor; + valueColorCustom.value = newValue.valueColor && newValue.valueColor !== 'auto' + ? newValue.valueColor + : '#FFFFFF'; showDescription.value = newValue.showDescription ?? true; nextTick(() => { syncingFromProps = false; @@ -173,6 +210,8 @@ export default defineComponent({ valueAppend, showHeader, valueFontSizeScale, + valueAlign, + valueColor, showDescription, ], emitValue, @@ -211,6 +250,47 @@ export default defineComponent({ }, }); + const valueColorEnabled = computed({ + get: () => valueColor.value !== undefined, + set: (enabled: boolean) => { + if (!enabled) { + valueColor.value = undefined; + return; + } + valueColor.value = valueColor.value || 'auto'; + }, + }); + + const valueColorAuto = computed({ + get: () => valueColor.value === 'auto', + set: (auto: boolean) => { + if (auto) { + valueColor.value = 'auto'; + return; + } + valueColor.value = valueColorCustom.value; + }, + }); + + const computedValueColorPreview = computed(() => { + if (!valueColor.value) { + return '#FFFFFF'; + } + if (valueColor.value === 'auto') { + if (props.attribute) { + return getAttributeValueColor(props.attribute); + } + return '#FFFFFF'; + } + return valueColor.value; + }); + + watch(valueColorCustom, (color) => { + if (valueColor.value !== undefined && valueColor.value !== 'auto') { + valueColor.value = color; + } + }); + return { enabled, showWithoutButtons, @@ -224,9 +304,18 @@ export default defineComponent({ valueAppend, showHeader, valueFontSizeScale, + valueAlign, + valueColor, + valueColorEnabled, + valueColorAuto, + valueColorCustom, + editingValueColor, + editingHighlightColor, + computedValueColorPreview, showDescription, valuePositionOptions, longValueModeOptions, + valueAlignOptions, fontSizeScaleOptions, highlightEnabled, highlightColorValue, @@ -397,22 +486,32 @@ export default defineComponent({ />
-
- - + + + + -
+ cols="auto" + class="py-0" + > +
+ + + + + + + + + + + + + Edit Value Color + + + + + + + + Close + + + + + + + + Edit Inherited Font Color + + + + + + + + Close + + + +
@@ -599,7 +805,17 @@ export default defineComponent({ } } -.highlight-color-picker { - max-width: 200px; +.value-color-box { + display: inline-block; + min-width: 36px; + max-width: 36px; + min-height: 36px; + max-height: 36px; + border: 1px solid rgba(0, 0, 0, 0.2); +} + +.edit-color-box:hover { + cursor: pointer; + border: 2px solid white; } diff --git a/client/dive-common/components/Attributes/AttributeEditor.vue b/client/dive-common/components/Attributes/AttributeEditor.vue index 4d00248a..6c0fa0ef 100644 --- a/client/dive-common/components/Attributes/AttributeEditor.vue +++ b/client/dive-common/components/Attributes/AttributeEditor.vue @@ -562,7 +562,7 @@ export default defineComponent({ /> - + import { defineComponent, PropType } from 'vue'; import { - getCustomUIDisplayValueFontSizeStyle, + getCustomUIDisplayValueStyle, getTruncatedCustomUIDisplayValue, LONG_VALUE_EXPAND_THRESHOLD, ResolvedAttributeCustomUI, @@ -19,6 +19,8 @@ export interface CustomUIValueEntry { valuePrepend?: string; valueAppend?: string; valueFontSizeScale: number; + valueAlign: NonNullable; + valueColorStyle: Record; } export default defineComponent({ @@ -40,7 +42,7 @@ export default defineComponent({ setup() { return { LONG_VALUE_EXPAND_THRESHOLD, - getCustomUIDisplayValueFontSizeStyle, + getCustomUIDisplayValueStyle, getTruncatedCustomUIDisplayValue, shouldUseCustomUIValueExpansion, }; @@ -49,6 +51,12 @@ export default defineComponent({ onPanelChange() { this.$emit('toggle-panel', this.attributeName); }, + getValueTextStyle(entry: CustomUIValueEntry) { + return { + ...entry.valueColorStyle, + ...entry.indicatorStyle, + }; + }, }, }); @@ -56,7 +64,7 @@ export default defineComponent({