feat(tms): インスタンス情報の表示位置 (taiyme#198)

This commit is contained in:
kakkokari-gtyih 2025-05-04 19:17:58 +09:00
parent 080276e3e7
commit 96012a59f8
5 changed files with 212 additions and 77 deletions

View File

@ -4,84 +4,77 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="$style.root" :style="themeColorStyle"> <div
<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/> :class="$style.root"
<div :class="$style.name">{{ instanceName }}</div> :style="tickerColorsRef"
>
<img :class="$style.icon" :src="tickerInfoRef.iconUrl"/>
<div :class="$style.name">{{ tickerInfoRef.name }}</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { instanceName as localInstanceName } from '@@/js/config.js'; import { getTickerColors, getTickerInfo } from '@/scripts/tms/instance-ticker.js';
import type { CSSProperties } from 'vue';
import { instance as localInstance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
const props = defineProps<{ export type TickerProps = {
host: string | null; readonly instance?: {
instance?: { readonly name?: string | null;
faviconUrl?: string | null readonly iconUrl?: string | null;
name?: string | null readonly faviconUrl?: string | null;
themeColor?: string | null readonly themeColor?: string | null;
} } | null;
}>(); readonly channel?: {
readonly name: string;
readonly color: string;
} | null;
};
// if no instance data is given, this is for the local instance const props = defineProps<TickerProps>();
const instanceName = computed(() => props.host == null ? localInstanceName : props.instance?.name ?? props.host);
const faviconUrl = computed(() => { const tickerInfoRef = computed(() => getTickerInfo(props));
let imageSrc: string | null = null; const tickerColorsRef = computed(() => getTickerColors(tickerInfoRef.value));
if (props.host == null) {
if (localInstance.iconUrl == null) {
return '/favicon.ico';
} else {
imageSrc = localInstance.iconUrl;
}
} else {
imageSrc = props.instance?.faviconUrl ?? null;
}
return getProxiedImageUrlNullable(imageSrc);
});
const themeColorStyle = computed<CSSProperties>(() => {
const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777';
return {
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
};
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>
$height: 2ex;
.root { .root {
display: flex; overflow: hidden;
align-items: center;
height: $height;
border-radius: 4px 0 0 4px;
overflow: clip; overflow: clip;
color: #fff; display: block;
box-sizing: border-box;
background-color: var(--ticker-bg, #777777);
color: var(--ticker-fg, #ffffff);
// text-shadow使 --ticker-size: 2ex;
display: grid;
align-items: center;
grid-template: var(--ticker-size) / var(--ticker-size) 1fr;
gap: 4px;
border-radius: 4px;
mask-image: linear-gradient(90deg, > .icon {
rgb(0,0,0), width: var(--ticker-size);
rgb(0,0,0) calc(100% - 16px), height: var(--ticker-size);
rgba(0,0,0,0) 100% }
);
> .name {
line-height: var(--ticker-size);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
} }
.icon { .icon {
height: $height; display: block;
flex-shrink: 0; aspect-ratio: 1 / 1;
box-sizing: border-box;
} }
.name { .name {
margin-left: 4px; display: block;
line-height: 1;
font-size: 0.9em; font-size: 0.9em;
font-weight: bold; font-weight: bold;
white-space: nowrap; box-sizing: border-box;
overflow: visible;
} }
</style> </style>

View File

@ -760,10 +760,6 @@ function emitUpdReaction(emoji: string, delta: number) {
& + .article { & + .article {
padding-top: 8px; padding-top: 8px;
} }
> .colorBar {
height: calc(100% - 6px);
}
} }
.renoteAvatar { .renoteAvatar {
@ -835,16 +831,6 @@ function emitUpdReaction(emoji: string, delta: number) {
padding: 28px 32px; padding: 28px 32px;
} }
.colorBar {
position: absolute;
top: 8px;
left: 8px;
width: 5px;
height: calc(100% - 16px);
border-radius: 999px;
pointer-events: none;
}
.avatar { .avatar {
flex-shrink: 0; flex-shrink: 0;
display: block !important; display: block !important;
@ -1068,13 +1054,6 @@ function emitUpdReaction(emoji: string, delta: number) {
} }
} }
} }
.colorBar {
top: 6px;
left: 6px;
width: 4px;
height: calc(100% - 12px);
}
} }
@container (max-width: 300px) { @container (max-width: 300px) {

View File

@ -60,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span> </span>
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
<span v-if="appearNote.channel" style="margin-left: 0.5em;" :title="appearNote.channel.name"><i class="ti ti-device-tv"></i></span>
</div> </div>
</div> </div>
<div :class="$style.noteHeaderUsernameAndBadgeRoles"> <div :class="$style.noteHeaderUsernameAndBadgeRoles">

View File

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type HEX = string;
export type RGB = {
readonly r: number;
readonly g: number;
readonly b: number;
};
const DEFAULT_HEX = '#ff0000' as const satisfies HEX;
const DEFAULT_RGB = { r: 255, g: 0, b: 0 } as const satisfies RGB;
export const hexToRgb = (hex: string): RGB => {
if (hex.startsWith('#')) {
hex = hex.slice(1);
}
if (hex.length === 3) {
if (!validHex(hex)) {
return DEFAULT_RGB;
}
hex = [...hex].map(char => char.repeat(2)).join('');
}
if (!(hex.length === 6 && validHex(hex))) {
return DEFAULT_RGB;
}
const [r, g, b] = Array.from(hex.match(/.{2}/g) ?? [], n => parseInt(n, 16));
return { r, g, b } as const satisfies RGB;
};
export const rgbToHex = (rgb: RGB): HEX => {
if (!validRgb(rgb)) {
return DEFAULT_HEX;
}
const toHex2Digit = (n: number): string => {
return (n.toString(16).split('.').at(0) ?? '').padStart(2, '0');
};
const { r, g, b } = rgb;
const hexR = toHex2Digit(r);
const hexG = toHex2Digit(g);
const hexB = toHex2Digit(b);
return `#${hexR}${hexG}${hexB}` as const satisfies HEX;
};
const validHex = (hex: unknown): hex is string => {
if (typeof hex !== 'string') return false;
return /^[0-9a-f]+$/i.test(hex);
};
const validRgb = (rgb: unknown): rgb is RGB => {
if (typeof rgb !== 'object' || rgb == null) return false;
if (!('r' in rgb && 'g' in rgb && 'b' in rgb)) return false;
const validRange = (n: unknown): boolean => {
if (typeof n !== 'number') return false;
if (!Number.isInteger(n)) return false;
return 0 <= n && n <= 255;
};
const { r, g, b } = rgb;
return validRange(r) && validRange(g) && validRange(b);
};

View File

@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { host } from '@/config.js';
import { instance as localInstance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
import { type HEX, hexToRgb } from '@/scripts/tms/color.js';
import { type TickerProps } from '@/components/TmsInstanceTicker.vue';
//#region ticker info
type TickerInfo = {
readonly name: string;
readonly iconUrl: string;
readonly themeColor: string;
};
const TICKER_BG_COLOR_DEFAULT = '#777777' as const;
export const getTickerInfo = (props: TickerProps): TickerInfo => {
if (props.channel != null) {
return {
name: props.channel.name,
iconUrl: getProxiedIconUrl(localInstance) ?? '/favicon.ico',
themeColor: props.channel.color,
} as const satisfies TickerInfo;
}
if (props.instance != null) {
return {
name: props.instance.name ?? '',
iconUrl: getProxiedIconUrl(props.instance) ?? '/client-assets/dummy.png',
themeColor: props.instance.themeColor ?? TICKER_BG_COLOR_DEFAULT,
} as const satisfies TickerInfo;
}
return {
name: localInstance.name ?? host,
iconUrl: getProxiedIconUrl(localInstance) ?? '/favicon.ico',
themeColor: localInstance.themeColor ?? document.querySelector<HTMLMetaElement>('meta[name="theme-color-orig"]')?.content ?? TICKER_BG_COLOR_DEFAULT,
} as const satisfies TickerInfo;
};
const getProxiedIconUrl = (instance: NonNullable<TickerProps['instance']>): string | null => {
return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? null;
};
//#endregion ticker info
//#region ticker colors
type TickerColors = {
readonly '--ticker-bg': string;
readonly '--ticker-fg': string;
readonly '--ticker-bg-rgb': string;
};
const TICKER_YUV_THRESHOLD = 191 as const;
const TICKER_FG_COLOR_LIGHT = '#ffffff' as const;
const TICKER_FG_COLOR_DARK = '#2f2f2fcc' as const;
const tickerColorsCache = new Map<HEX, TickerColors>();
export const getTickerColors = (info: TickerInfo): TickerColors => {
const bgHex = info.themeColor;
const cachedTickerColors = tickerColorsCache.get(bgHex);
if (cachedTickerColors != null) return cachedTickerColors;
const { r, g, b } = hexToRgb(bgHex);
const yuv = 0.299 * r + 0.587 * g + 0.114 * b;
const fgHex = yuv > TICKER_YUV_THRESHOLD ? TICKER_FG_COLOR_DARK : TICKER_FG_COLOR_LIGHT;
const tickerColors = {
'--ticker-fg': fgHex,
'--ticker-bg': bgHex,
'--ticker-bg-rgb': `${r}, ${g}, ${b}`,
} as const satisfies TickerColors;
tickerColorsCache.set(bgHex, tickerColors);
return tickerColors;
};
//#endregion ticker colors
//#region ticker state
type TickerState = {
readonly normal: boolean;
readonly vertical: boolean;
readonly watermark: boolean;
readonly left: boolean;
readonly right: boolean;
};
export const getTickerState = (props: TickerProps): TickerState => {
const vertical = props.position === 'leftVerticalBar' || props.position === 'rightVerticalBar';
const watermark = props.position === 'leftWatermark' || props.position === 'rightWatermark';
const normal = !vertical && !watermark;
const left = props.position === 'leftVerticalBar' || props.position === 'leftWatermark';
const right = props.position === 'rightVerticalBar' || props.position === 'rightWatermark';
return { normal, vertical, watermark, left, right } as const satisfies TickerState;
};
//#endregion ticker state