実装を簡略化

This commit is contained in:
kakkokari-gtyih 2025-05-04 20:03:05 +09:00
parent a41be520f1
commit 9f27689a92
3 changed files with 69 additions and 158 deletions

View File

@ -3,85 +3,34 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { host } from '@@/js/config.js';
import { instance as localInstance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
import { hexToRgb } from '@/utility/color.js';
import type { HEX } from '@/utility/color.js';
import tinycolor from 'tinycolor2';
export type MkInstanceTickerProps = {
readonly instance?: {
readonly name?: string | null;
readonly faviconUrl?: string | null;
readonly themeColor?: string | null;
} | null;
readonly channel?: {
readonly name: string;
readonly color: string;
} | null;
};
//#region ticker info
type ITickerInfo = {
readonly name: string;
readonly iconUrl: string;
readonly themeColor: string;
};
const TICKER_BG_COLOR_DEFAULT = '#777777' as const;
export const getTickerInfo = (props: MkInstanceTickerProps): ITickerInfo => {
if (props.channel != null) {
return {
name: props.channel.name,
iconUrl: getProxiedImageUrlNullable(localInstance.iconUrl, 'preview') ?? '/favicon.ico',
themeColor: props.channel.color,
} as const satisfies ITickerInfo;
}
if (props.instance != null) {
return {
name: props.instance.name ?? '',
// NOTE: リモートサーバーにおいてiconUrlを参照すると意図した画像にならない https://github.com/taiyme/misskey/issues/210
iconUrl: getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png',
themeColor: props.instance.themeColor ?? TICKER_BG_COLOR_DEFAULT,
} as const satisfies ITickerInfo;
}
return {
name: localInstance.name ?? host,
iconUrl: getProxiedImageUrlNullable(localInstance.iconUrl, 'preview') ?? '/favicon.ico',
themeColor: localInstance.themeColor ?? window.document.querySelector<HTMLMetaElement>('meta[name="theme-color-orig"]')?.content ?? TICKER_BG_COLOR_DEFAULT,
} as const satisfies ITickerInfo;
};
//#endregion ticker info
//#region ticker colors
type ITickerColors = {
readonly '--ticker-bg': string;
readonly '--ticker-fg': string;
readonly bg: string;
readonly fg: 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, ITickerColors>();
const tickerColorsCache = new Map<string, ITickerColors>();
export const getTickerColors = (info: ITickerInfo): ITickerColors => {
const bgHex = info.themeColor;
export const getTickerColors = (bgHex: string): ITickerColors => {
const cachedTickerColors = tickerColorsCache.get(bgHex);
if (cachedTickerColors != null) return cachedTickerColors;
const { r, g, b } = hexToRgb(bgHex);
const tinycolorInstance = tinycolor(bgHex);
const { r, g, b } = tinycolorInstance.toRgb();
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,
fg: fgHex,
bg: bgHex,
} as const satisfies ITickerColors;
tickerColorsCache.set(bgHex, tickerColors);
tickerColorsCache.set(tinycolorInstance.toHex(), tickerColors);
return tickerColors;
};
//#endregion ticker colors

View File

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

View File

@ -3,17 +3,6 @@
* 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 alpha = (hex: string, a: number): string => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
const r = parseInt(result[1], 16);
@ -21,51 +10,3 @@ export const alpha = (hex: string, a: number): string => {
const b = parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
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);
};