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>
|
<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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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