実装を簡略化
This commit is contained in:
parent
a41be520f1
commit
9f27689a92
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue