From 96012a59f81f3c63811590e68a3c44ce317f2813 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 4 May 2025 19:17:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(tms):=20=E3=82=A4=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=82=B9=E6=83=85=E5=A0=B1=E3=81=AE=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E4=BD=8D=E7=BD=AE=20(taiyme#198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/MkInstanceTicker.vue | 105 ++++++++---------- packages/frontend/src/components/MkNote.vue | 21 ---- .../src/components/MkNoteDetailed.vue | 1 + packages/frontend/src/scripts/tms/color.ts | 63 +++++++++++ .../src/scripts/tms/instance-ticker.ts | 99 +++++++++++++++++ 5 files changed, 212 insertions(+), 77 deletions(-) create mode 100644 packages/frontend/src/scripts/tms/color.ts create mode 100644 packages/frontend/src/scripts/tms/instance-ticker.ts diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 380fb7b2d8..32151c0e4d 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -4,84 +4,77 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 980636f551..7df43b79b3 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -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) { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 17a348affe..04ee20e27b 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -60,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only +
diff --git a/packages/frontend/src/scripts/tms/color.ts b/packages/frontend/src/scripts/tms/color.ts new file mode 100644 index 0000000000..9251dae3b4 --- /dev/null +++ b/packages/frontend/src/scripts/tms/color.ts @@ -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); +}; diff --git a/packages/frontend/src/scripts/tms/instance-ticker.ts b/packages/frontend/src/scripts/tms/instance-ticker.ts new file mode 100644 index 0000000000..f4cb9d562f --- /dev/null +++ b/packages/frontend/src/scripts/tms/instance-ticker.ts @@ -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('meta[name="theme-color-orig"]')?.content ?? TICKER_BG_COLOR_DEFAULT, + } as const satisfies TickerInfo; +}; + +const getProxiedIconUrl = (instance: NonNullable): 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(); + +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