feat(tms): インスタンス情報の表示位置 (taiyme#198)
This commit is contained in:
parent
080276e3e7
commit
96012a59f8
|
@ -4,84 +4,77 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root" :style="themeColorStyle">
|
||||
<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/>
|
||||
<div :class="$style.name">{{ instanceName }}</div>
|
||||
<div
|
||||
:class="$style.root"
|
||||
:style="tickerColorsRef"
|
||||
>
|
||||
<img :class="$style.icon" :src="tickerInfoRef.iconUrl"/>
|
||||
<div :class="$style.name">{{ tickerInfoRef.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
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, getTickerInfo } from '@/scripts/tms/instance-ticker.js';
|
||||
|
||||
const props = defineProps<{
|
||||
host: string | null;
|
||||
instance?: {
|
||||
faviconUrl?: string | null
|
||||
name?: string | null
|
||||
themeColor?: string | null
|
||||
}
|
||||
}>();
|
||||
|
||||
// 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';
|
||||
return {
|
||||
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
|
||||
export type TickerProps = {
|
||||
readonly instance?: {
|
||||
readonly name?: string | null;
|
||||
readonly iconUrl?: string | null;
|
||||
readonly faviconUrl?: string | null;
|
||||
readonly themeColor?: string | null;
|
||||
} | null;
|
||||
readonly channel?: {
|
||||
readonly name: string;
|
||||
readonly color: string;
|
||||
} | null;
|
||||
};
|
||||
});
|
||||
|
||||
const props = defineProps<TickerProps>();
|
||||
|
||||
const tickerInfoRef = computed(() => getTickerInfo(props));
|
||||
const tickerColorsRef = computed(() => getTickerColors(tickerInfoRef.value));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
$height: 2ex;
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: $height;
|
||||
border-radius: 4px 0 0 4px;
|
||||
overflow: hidden;
|
||||
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,
|
||||
rgb(0,0,0),
|
||||
rgb(0,0,0) calc(100% - 16px),
|
||||
rgba(0,0,0,0) 100%
|
||||
);
|
||||
> .icon {
|
||||
width: var(--ticker-size);
|
||||
height: var(--ticker-size);
|
||||
}
|
||||
|
||||
> .name {
|
||||
line-height: var(--ticker-size);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: $height;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
aspect-ratio: 1 / 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 4px;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -760,10 +760,6 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
& + .article {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
> .colorBar {
|
||||
height: calc(100% - 6px);
|
||||
}
|
||||
}
|
||||
|
||||
.renoteAvatar {
|
||||
|
@ -835,16 +831,6 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.colorBar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 5px;
|
||||
height: calc(100% - 16px);
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
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) {
|
||||
|
|
|
@ -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>
|
||||
</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 :class="$style.noteHeaderUsernameAndBadgeRoles">
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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
|
Loading…
Reference in New Issue