feat: 调整微信风格明暗主题并完整展示详情图片
This commit is contained in:
+34
-25
@@ -1,25 +1,34 @@
|
||||
/* 管理后台视口边界:document 不滚动,各个工作区和面板独立滚动。 */
|
||||
:root {
|
||||
--app-ink: #202020;
|
||||
--app-muted: #737373;
|
||||
--app-ink: #191919;
|
||||
--app-muted: #707070;
|
||||
--app-body: #ededed;
|
||||
--app-surface: #ffffff;
|
||||
--app-subtle: #f5f5f5;
|
||||
--app-subtle: #f7f7f7;
|
||||
--app-border: #e5e5e5;
|
||||
--app-inverse: #ffffff;
|
||||
--app-accent: #07c160;
|
||||
--app-accent-hover: #2dcb79;
|
||||
--app-accent-text: #087c42;
|
||||
--app-on-accent: #082b17;
|
||||
--app-selected: #e4f4e9;
|
||||
--app-shadow: #00000018;
|
||||
--app-danger: #b42318;
|
||||
--app-success: #447454;
|
||||
--app-danger: #a93232;
|
||||
--app-success: #087c42;
|
||||
}
|
||||
:root[data-theme='dark'] {
|
||||
--app-ink: #ededed;
|
||||
--app-ink: #d9d9d9;
|
||||
--app-muted: #a3a3a3;
|
||||
--app-surface: #1c1c1c;
|
||||
--app-subtle: #252525;
|
||||
--app-border: #363636;
|
||||
--app-inverse: #171717;
|
||||
--app-body: #111111;
|
||||
--app-surface: #191919;
|
||||
--app-subtle: #232323;
|
||||
--app-border: #333333;
|
||||
--app-inverse: #111111;
|
||||
--app-accent-text: #5cd693;
|
||||
--app-selected: #173527;
|
||||
--app-shadow: #00000055;
|
||||
--app-danger: #ee8b83;
|
||||
--app-success: #91ba9b;
|
||||
--app-danger: #fa7373;
|
||||
--app-success: #5cd693;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
@@ -30,7 +39,7 @@ body,
|
||||
overflow: hidden;
|
||||
}
|
||||
html {
|
||||
background: var(--app-surface);
|
||||
background: var(--app-body);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -515,11 +524,11 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.n-button.directory-item.selected {
|
||||
background: var(--app-surface);
|
||||
box-shadow: inset 3px 0 var(--app-ink);
|
||||
background: var(--app-selected);
|
||||
box-shadow: inset 3px 0 var(--app-accent);
|
||||
}
|
||||
.n-button.directory-item:focus-visible {
|
||||
outline: 2px solid var(--app-ink);
|
||||
outline: 2px solid var(--app-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
/* Tabs 的默认整行宽度限制在左侧网格内,导出按钮始终处于同一行右端。 */
|
||||
@@ -695,9 +704,9 @@ body {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.n-button.reader-episode-link.active {
|
||||
background: var(--app-surface);
|
||||
color: var(--app-ink);
|
||||
box-shadow: inset 3px 0 var(--app-ink);
|
||||
background: var(--app-selected);
|
||||
color: var(--app-accent-text);
|
||||
box-shadow: inset 3px 0 var(--app-accent);
|
||||
}
|
||||
.reader-page {
|
||||
min-width: 0;
|
||||
@@ -706,7 +715,7 @@ body {
|
||||
background: var(--app-surface);
|
||||
}
|
||||
.reader-scroll .n-scrollbar-container:focus-visible {
|
||||
outline: 2px solid var(--app-ink);
|
||||
outline: 2px solid var(--app-accent);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
.reader-content {
|
||||
@@ -922,8 +931,8 @@ body {
|
||||
white-space: normal;
|
||||
}
|
||||
.n-button.image-history-item.selected {
|
||||
background: var(--app-subtle);
|
||||
box-shadow: inset 0 0 0 2px var(--app-ink);
|
||||
background: var(--app-selected);
|
||||
box-shadow: inset 0 0 0 2px var(--app-accent);
|
||||
}
|
||||
.n-button.image-history-item .n-button__content {
|
||||
display: block;
|
||||
@@ -973,8 +982,8 @@ body {
|
||||
background: var(--app-surface);
|
||||
}
|
||||
.n-button.casting-item.selected {
|
||||
background: var(--app-subtle);
|
||||
box-shadow: inset 3px 0 0 var(--app-ink);
|
||||
background: var(--app-selected);
|
||||
box-shadow: inset 3px 0 0 var(--app-accent);
|
||||
}
|
||||
.n-button.casting-item .n-button__content {
|
||||
display: flex;
|
||||
@@ -1043,7 +1052,7 @@ body {
|
||||
background: #080808;
|
||||
}
|
||||
.button-primary {
|
||||
color: var(--app-inverse);
|
||||
color: var(--app-on-accent);
|
||||
}
|
||||
.skip-link:focus {
|
||||
z-index: 3000;
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('管理后台组件边界', () => {
|
||||
)
|
||||
expect(css).toMatch(/\.casting-item-name\s*\{[^}]*overflow-wrap:\s*anywhere/)
|
||||
expect(css).toMatch(/\.casting-item-status\s*\{[^}]*flex-shrink:\s*0/)
|
||||
expect(css).toMatch(/\.n-button\.casting-item\.selected\s*\{[^}]*var\(--app-subtle\)[^}]*var\(--app-ink\)/)
|
||||
expect(css).toMatch(/\.n-button\.casting-item\.selected\s*\{[^}]*var\(--app-selected\)[^}]*var\(--app-accent\)/)
|
||||
})
|
||||
it('拆解分栏跟随剩余高度收缩,移动端历史区不挤出结果区', () => {
|
||||
// DOM 环境不计算几何,保护完整的 flex → grid → NScrollbar 高度链。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { effectScope, nextTick, type EffectScope } from 'vue'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useTheme } from './useTheme'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
let scope: EffectScope | undefined
|
||||
afterEach(() => {
|
||||
@@ -30,7 +31,22 @@ function setup(dark = false) {
|
||||
}
|
||||
}
|
||||
|
||||
describe('黑白双主题', () => {
|
||||
/** 将十六进制颜色换算为 sRGB 相对亮度。 */
|
||||
function luminance(hex: string) {
|
||||
const channels = [1, 3, 5].map(index => {
|
||||
const value = parseInt(hex.slice(index, index + 2), 16) / 255
|
||||
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4
|
||||
})
|
||||
return channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722
|
||||
}
|
||||
|
||||
/** 检查主题文字对比度,避免绿色按钮和浅色选中项难以辨认。 */
|
||||
function contrast(foreground: string, background: string) {
|
||||
const values = [luminance(foreground), luminance(background)]
|
||||
return (Math.max(...values) + 0.05) / (Math.min(...values) + 0.05)
|
||||
}
|
||||
|
||||
describe('微信风格明暗主题', () => {
|
||||
it('默认跟随系统并实时响应,销毁时移除监听', async () => {
|
||||
const state = setup()
|
||||
expect(state.preference.value).toBe('system')
|
||||
@@ -60,15 +76,71 @@ describe('黑白双主题', () => {
|
||||
|
||||
it('深浅模式同时更新 Naive 主题和业务颜色标记', async () => {
|
||||
const state = setup()
|
||||
expect(state.overrides.value.common?.primaryColor).toBe('#202020')
|
||||
expect(state.overrides.value.common?.primaryColor).toBe('#07c160')
|
||||
expect(state.overrides.value.common?.bodyColor).toBe('#ededed')
|
||||
state.preference.value = 'dark'
|
||||
await nextTick()
|
||||
expect(state.theme.value?.name).toBe('dark')
|
||||
expect(state.overrides.value.common?.primaryColor).toBe('#f5f5f5')
|
||||
expect(state.overrides.value.Button?.textColorPrimary).toBe('#171717')
|
||||
expect(state.overrides.value.common?.primaryColor).toBe('#07c160')
|
||||
expect(state.overrides.value.common?.bodyColor).toBe('#111111')
|
||||
expect(state.overrides.value.Button?.textColorPrimary).toBe('#082b17')
|
||||
expect(document.documentElement.dataset.theme).toBe('dark')
|
||||
})
|
||||
|
||||
it.each([false, true])('明暗模式 %s 的组件表面、语义色与业务 CSS 一致,主要文字保持可读', dark => {
|
||||
const state = setup(dark)
|
||||
const { common, Menu, Button, Layout } = state.overrides.value
|
||||
const css = readFileSync('src/admin.css', 'utf8')
|
||||
const lightTokens = css.match(/:root\s*\{([^}]+)\}/)![1]!
|
||||
const darkTokens = css.match(/:root\[data-theme='dark'\]\s*\{([^}]+)\}/)![1]!
|
||||
const tokens = Object.fromEntries(
|
||||
[...(lightTokens + (dark ? darkTokens : '')).matchAll(/--app-([\w-]+):\s*(#[\da-f]+);/g)].map(match => [
|
||||
match[1],
|
||||
match[2]
|
||||
])
|
||||
)
|
||||
expect(common).toMatchObject({
|
||||
bodyColor: tokens.body,
|
||||
cardColor: tokens.surface,
|
||||
modalColor: tokens.surface,
|
||||
tableColor: tokens.surface,
|
||||
inputColor: tokens.surface,
|
||||
borderColor: tokens.border,
|
||||
textColor1: tokens.ink,
|
||||
textColor3: tokens.muted,
|
||||
primaryColor: tokens.accent,
|
||||
successColor: tokens.success,
|
||||
errorColor: tokens.danger
|
||||
})
|
||||
expect(Layout).toMatchObject({ color: tokens.body, headerColor: tokens.surface, siderColor: tokens.subtle })
|
||||
expect(Menu).toMatchObject({ itemColorActive: tokens.selected, itemTextColorActive: tokens['accent-text'] })
|
||||
expect(Button?.textColorPrimary).toBe(tokens['on-accent'])
|
||||
for (const background of [common!.primaryColor!, common!.primaryColorHover!, common!.primaryColorPressed!]) {
|
||||
expect(contrast(String(Button!.textColorPrimary), background)).toBeGreaterThanOrEqual(4.5)
|
||||
}
|
||||
expect(contrast(tokens.ink!, tokens.surface!)).toBeGreaterThanOrEqual(4.5)
|
||||
expect(contrast(tokens.muted!, tokens.subtle!)).toBeGreaterThanOrEqual(4.5)
|
||||
expect(contrast(tokens['accent-text']!, tokens.selected!)).toBeGreaterThanOrEqual(4.5)
|
||||
})
|
||||
|
||||
it('系统切换时浏览器主题色与页面同步', async () => {
|
||||
const meta = document.createElement('meta')
|
||||
meta.name = 'theme-color'
|
||||
document.head.appendChild(meta)
|
||||
try {
|
||||
const state = setup()
|
||||
expect(meta.content).toBe('#ededed')
|
||||
state.change(true)
|
||||
await nextTick()
|
||||
expect(meta.content).toBe('#111111')
|
||||
state.preference.value = 'light'
|
||||
await nextTick()
|
||||
expect(meta.content).toBe('#ededed')
|
||||
} finally {
|
||||
meta.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('无效存储回退系统模式', () => {
|
||||
localStorage.setItem('drama-studio-theme', 'unexpected')
|
||||
const state = setup(true)
|
||||
|
||||
+70
-27
@@ -24,48 +24,88 @@ export function useTheme() {
|
||||
const theme = computed(() => (isDark.value ? darkTheme : null))
|
||||
const overrides = computed<GlobalThemeOverrides>(() => {
|
||||
const dark = isDark.value
|
||||
const primary = dark ? '#f5f5f5' : '#202020'
|
||||
const hover = dark ? '#d4d4d4' : '#444444'
|
||||
// 微信风格灰阶表面与绿色操作态;文字绿独立加深,避免浅底小字对比不足。
|
||||
const primary = '#07c160'
|
||||
const hover = '#2dcb79'
|
||||
const pressed = '#06ad56'
|
||||
const onPrimary = '#082b17'
|
||||
const accentText = dark ? '#5cd693' : '#087c42'
|
||||
const body = dark ? '#111111' : '#ededed'
|
||||
const surface = dark ? '#191919' : '#ffffff'
|
||||
const subtle = dark ? '#232323' : '#f7f7f7'
|
||||
const text = dark ? '#d9d9d9' : '#191919'
|
||||
const muted = dark ? '#a3a3a3' : '#707070'
|
||||
const selected = dark ? '#173527' : '#e4f4e9'
|
||||
const info = dark ? '#91a8d2' : '#576b95'
|
||||
const danger = dark ? '#fa7373' : '#a93232'
|
||||
const warning = dark ? '#e8b66a' : '#916019'
|
||||
return {
|
||||
common: {
|
||||
primaryColor: primary,
|
||||
primaryColorHover: hover,
|
||||
primaryColorPressed: hover,
|
||||
primaryColorPressed: pressed,
|
||||
primaryColorSuppl: primary,
|
||||
infoColor: primary,
|
||||
infoColorHover: hover,
|
||||
infoColorPressed: hover,
|
||||
infoColorSuppl: primary,
|
||||
infoColor: info,
|
||||
infoColorHover: info,
|
||||
infoColorPressed: info,
|
||||
infoColorSuppl: info,
|
||||
successColor: accentText,
|
||||
successColorHover: accentText,
|
||||
successColorPressed: accentText,
|
||||
successColorSuppl: accentText,
|
||||
errorColor: danger,
|
||||
errorColorHover: danger,
|
||||
errorColorPressed: danger,
|
||||
errorColorSuppl: danger,
|
||||
warningColor: warning,
|
||||
warningColorHover: warning,
|
||||
warningColorPressed: warning,
|
||||
warningColorSuppl: warning,
|
||||
borderRadius: '6px',
|
||||
bodyColor: dark ? '#141414' : '#f5f5f5',
|
||||
cardColor: dark ? '#1c1c1c' : '#ffffff',
|
||||
modalColor: dark ? '#202020' : '#ffffff',
|
||||
popoverColor: dark ? '#262626' : '#ffffff',
|
||||
tableColor: dark ? '#1c1c1c' : '#ffffff',
|
||||
borderColor: dark ? '#363636' : '#e5e5e5',
|
||||
textColorBase: dark ? '#ededed' : '#202020',
|
||||
bodyColor: body,
|
||||
cardColor: surface,
|
||||
modalColor: surface,
|
||||
popoverColor: dark ? '#2c2c2c' : surface,
|
||||
tableColor: surface,
|
||||
inputColor: surface,
|
||||
actionColor: subtle,
|
||||
borderColor: dark ? '#333333' : '#e5e5e5',
|
||||
dividerColor: dark ? '#333333' : '#e5e5e5',
|
||||
hoverColor: subtle,
|
||||
textColorBase: text,
|
||||
textColor1: text,
|
||||
textColor2: text,
|
||||
textColor3: muted,
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif"
|
||||
},
|
||||
Button: {
|
||||
textColorPrimary: dark ? '#171717' : '#ffffff',
|
||||
textColorHoverPrimary: dark ? '#171717' : '#ffffff',
|
||||
textColorPressedPrimary: dark ? '#171717' : '#ffffff'
|
||||
textColorPrimary: onPrimary,
|
||||
textColorHoverPrimary: onPrimary,
|
||||
textColorPressedPrimary: onPrimary,
|
||||
textColorFocusPrimary: onPrimary,
|
||||
textColorDisabledPrimary: onPrimary,
|
||||
textColorTextPrimary: accentText,
|
||||
textColorGhostPrimary: accentText
|
||||
},
|
||||
Layout: {
|
||||
color: dark ? '#141414' : '#f5f5f5',
|
||||
headerColor: dark ? '#1c1c1c' : '#ffffff',
|
||||
siderColor: dark ? '#1c1c1c' : '#ffffff'
|
||||
color: body,
|
||||
headerColor: surface,
|
||||
siderColor: subtle
|
||||
},
|
||||
Menu: {
|
||||
itemColorActive: dark ? '#333333' : '#eeeeee',
|
||||
itemColorActiveHover: dark ? '#383838' : '#e7e7e7',
|
||||
itemTextColorActive: primary,
|
||||
itemTextColorActiveHover: primary,
|
||||
itemIconColorActive: primary,
|
||||
itemIconColorActiveHover: primary
|
||||
itemColorActive: selected,
|
||||
itemColorActiveHover: selected,
|
||||
itemColorHover: dark ? '#2c2c2c' : '#e8e8e8',
|
||||
itemTextColorActive: accentText,
|
||||
itemTextColorActiveHover: accentText,
|
||||
itemIconColorActive: accentText,
|
||||
itemIconColorActiveHover: accentText
|
||||
},
|
||||
Checkbox: { checkMarkColor: dark ? '#171717' : '#ffffff' },
|
||||
Tabs: { tabTextColorActiveLine: accentText, tabTextColorHoverLine: accentText, barColor: primary },
|
||||
Tag: { textColorPrimary: accentText },
|
||||
Checkbox: { checkMarkColor: onPrimary },
|
||||
Switch: { railColorActive: primary },
|
||||
Progress: { fillColor: primary }
|
||||
}
|
||||
})
|
||||
@@ -78,6 +118,9 @@ export function useTheme() {
|
||||
watchEffect(() => {
|
||||
document.documentElement.dataset.theme = isDark.value ? 'dark' : 'light'
|
||||
document.documentElement.style.colorScheme = isDark.value ? 'dark' : 'light'
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute('content', isDark.value ? '#111111' : '#ededed')
|
||||
try {
|
||||
localStorage.setItem(storageKey, preference.value)
|
||||
} catch {
|
||||
|
||||
@@ -64,6 +64,7 @@ function chooseAnchor() {
|
||||
:src="selected.status === 'completed' ? selected.imageUrl : null"
|
||||
:alt="`${subjectName} · ${identityViewLabels[selected.viewType]}`"
|
||||
class="asset-image-preview mt-4"
|
||||
object-fit="contain"
|
||||
preview
|
||||
:empty-text="selected.status === 'failed' ? '本次生成失败' : '等待后端图片结果'"
|
||||
/>
|
||||
|
||||
@@ -45,7 +45,7 @@ function input(selector: string, value: string) {
|
||||
}
|
||||
|
||||
describe('身份图与母版契约', () => {
|
||||
it('大图和历史统一 cover,缩略图只切换记录,大图单独打开完整原图', async () => {
|
||||
it('详情大图 contain 完整显示,历史仍为 cover,缩略图只切换记录', async () => {
|
||||
wrapper = mount(IdentityGallery, {
|
||||
attachTo: document.body,
|
||||
props: {
|
||||
@@ -66,13 +66,14 @@ describe('身份图与母版契约', () => {
|
||||
})
|
||||
const images = wrapper.findAllComponents(NImage)
|
||||
expect(images).toHaveLength(3)
|
||||
expect(images.every(image => image.props('objectFit') === 'cover')).toBe(true)
|
||||
expect(images.map(image => image.props('objectFit'))).toEqual(['contain', 'cover', 'cover'])
|
||||
expect(images.map(image => image.props('previewDisabled'))).toEqual([false, true, true])
|
||||
await wrapper.get('[aria-label="查看身份图片 front"] img').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.get('[aria-label="查看身份图片 front"]').attributes('aria-pressed')).toBe('true')
|
||||
expect(document.querySelector('.n-image-preview-container')).toBeNull()
|
||||
const main = wrapper.get('.asset-image-preview img')
|
||||
expect((main.element as HTMLImageElement).style.objectFit).toBe('contain')
|
||||
expect(main.attributes('src')).toContain('/storage/front.png')
|
||||
await main.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
@@ -101,6 +101,7 @@ async function choosePrimary() {
|
||||
:alt="`${form?.subject.name} · ${form?.name}`"
|
||||
:empty-text="selected.status === 'failed' ? '本次生成失败' : '等待后端生成结果'"
|
||||
class="asset-image-preview"
|
||||
object-fit="contain"
|
||||
preview
|
||||
/>
|
||||
<div class="image-history-heading">
|
||||
|
||||
@@ -39,6 +39,8 @@ describe('形态图片选择与操作', () => {
|
||||
await flushPromises()
|
||||
const history = document.querySelector<HTMLElement>('[aria-label="图片历史"]')!
|
||||
expect(history.querySelectorAll('.image-history-item')).toHaveLength(3)
|
||||
expect(document.querySelector<HTMLImageElement>('.asset-image-preview img')?.style.objectFit).toBe('contain')
|
||||
expect([...history.querySelectorAll('img')].every(image => image.style.objectFit === 'cover')).toBe(true)
|
||||
expect(history.previousElementSibling?.textContent).toContain('历史记录 · 3 条')
|
||||
expect(history.previousElementSibling?.previousElementSibling?.classList.contains('asset-image-preview')).toBe(
|
||||
true
|
||||
@@ -49,6 +51,7 @@ describe('形态图片选择与操作', () => {
|
||||
'/storage/candidate.png'
|
||||
)
|
||||
expect(history.querySelector('[aria-label="查看图片 candidate"]')?.getAttribute('aria-pressed')).toBe('true')
|
||||
expect(document.querySelector<HTMLImageElement>('.asset-image-preview img')?.style.objectFit).toBe('contain')
|
||||
history.querySelector<HTMLButtonElement>('[aria-label="查看图片 failed"]')!.click()
|
||||
await flushPromises()
|
||||
expect(document.querySelector('.asset-image-preview')?.textContent).toContain('本次生成失败')
|
||||
|
||||
+9
-8
@@ -1,12 +1,12 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* 黑白工作台主题:业务排版保留,全部颜色由语义变量控制。 */
|
||||
/* 微信风格明暗主题:灰阶表面搭配绿色操作态,业务排版保持不变。 */
|
||||
@theme {
|
||||
--color-ink: var(--app-ink);
|
||||
--color-muted: var(--app-muted);
|
||||
--color-faint: var(--app-muted);
|
||||
--color-line: var(--app-border);
|
||||
--color-accent: var(--app-ink);
|
||||
--color-accent: var(--app-accent-text);
|
||||
--color-danger: var(--app-danger);
|
||||
--color-success: var(--app-success);
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--app-subtle);
|
||||
background: var(--app-body);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -57,7 +57,8 @@
|
||||
outline-offset: 1px;
|
||||
}
|
||||
::selection {
|
||||
background: var(--app-border);
|
||||
background: var(--app-selected);
|
||||
color: var(--app-ink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,12 +152,12 @@
|
||||
border-color 0.15s;
|
||||
}
|
||||
.button-primary {
|
||||
background: var(--color-accent);
|
||||
color: var(--app-inverse);
|
||||
border-color: var(--color-accent);
|
||||
background: var(--app-accent);
|
||||
color: var(--app-on-accent);
|
||||
border-color: var(--app-accent);
|
||||
}
|
||||
.button-primary:hover:not(:disabled) {
|
||||
background: var(--app-ink);
|
||||
background: var(--app-accent-hover);
|
||||
}
|
||||
.button-secondary {
|
||||
background: var(--app-surface);
|
||||
|
||||
Reference in New Issue
Block a user