Files
VRCX/src/shared/utils/base/ui.js
2026-03-13 20:04:32 +09:00

474 lines
13 KiB
JavaScript

import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import {
APP_FONT_CONFIG,
APP_FONT_DEFAULT_KEY,
THEME_COLORS,
THEME_CONFIG
} from '../../constants';
import { i18n } from '../../../plugins/i18n';
import { router } from '../../../plugins/router';
import { textToHex } from './string';
import { useAppearanceSettingsStore } from '../../../stores';
import configRepository from '../../../services/config.js';
const THEME_COLOR_STORAGE_KEY = 'VRCX_themeColor';
const THEME_COLOR_STYLE_ID = 'app-theme-color-style';
const THEME_MODE_STYLE_ID = 'app-theme-mode-style';
const DEFAULT_THEME_COLOR_KEY = 'default';
const APP_FONT_LINK_ATTR = 'data-app-font';
const themeColors = THEME_COLORS.map((theme) => ({
...theme,
href: theme.file
? new URL(`../../../styles/themes/${theme.file}`, import.meta.url).href
: null
}));
const currentThemeColor = ref(DEFAULT_THEME_COLOR_KEY);
const isApplyingThemeColor = ref(false);
function resolveThemeColor(themeKey) {
const normalized = String(themeKey).trim().toLowerCase();
return (
themeColors.find((theme) => theme.key === normalized) ||
themeColors.find((theme) => theme.key === DEFAULT_THEME_COLOR_KEY)
);
}
function applyThemeColorStyle(theme) {
const root = document.documentElement;
root.setAttribute('data-theme-color', theme.key);
let styleEl = document.getElementById(THEME_COLOR_STYLE_ID);
if (!theme.href) {
styleEl?.remove();
return;
}
if (!styleEl) {
styleEl = document.createElement('link');
styleEl.id = THEME_COLOR_STYLE_ID;
styleEl.rel = 'stylesheet';
document.head.appendChild(styleEl);
}
if (styleEl.getAttribute('href') !== theme.href) {
styleEl.setAttribute('href', theme.href);
}
}
async function applyThemeColor(themeKey, { persist = true } = {}) {
const resolved = resolveThemeColor(themeKey);
isApplyingThemeColor.value = true;
applyThemeColorStyle(resolved);
currentThemeColor.value = resolved.key;
if (persist) {
try {
await configRepository.setString(
THEME_COLOR_STORAGE_KEY,
resolved.key
);
} catch (error) {
console.warn('Failed to persist theme color', error);
}
}
isApplyingThemeColor.value = false;
return resolved;
}
async function initThemeColor() {
const storedKey = await configRepository.getString(THEME_COLOR_STORAGE_KEY);
const resolved = resolveThemeColor(storedKey || DEFAULT_THEME_COLOR_KEY);
applyThemeColorStyle(resolved);
currentThemeColor.value = resolved.key;
}
function useThemeColor() {
return {
themeColors,
currentThemeColor,
isApplyingThemeColor,
applyThemeColor,
initThemeColor
};
}
/**
*
* @returns {boolean}
*/
function systemIsDarkMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function applyThemeFonts(themeKey, fontLinks = []) {
document
.querySelectorAll('link[data-theme-font]')
.forEach((linkEl) => linkEl.remove());
if (!fontLinks?.length) {
return;
}
const head = document.head;
fontLinks.forEach((href) => {
if (!href) {
return;
}
const fontLink = document.createElement('link');
fontLink.rel = 'stylesheet';
fontLink.href = href;
fontLink.dataset.themeFont = themeKey;
head.appendChild(fontLink);
});
}
function applyThemeModeStyle(themeMode) {
const themeConfig = THEME_CONFIG[themeMode];
const themeFile = themeConfig?.file;
let styleEl = document.getElementById(THEME_MODE_STYLE_ID);
if (!themeFile) {
styleEl?.remove();
return;
}
const themeHref = new URL(
`../../../styles/themes/${themeFile}`,
import.meta.url
).href;
if (!styleEl) {
styleEl = document.createElement('link');
styleEl.id = THEME_MODE_STYLE_ID;
styleEl.rel = 'stylesheet';
document.head.appendChild(styleEl);
}
if (styleEl.getAttribute('href') !== themeHref) {
styleEl.setAttribute('href', themeHref);
}
}
function resolveAppFontFamily(fontKey) {
const normalized = String(fontKey || '')
.trim()
.toLowerCase();
if (APP_FONT_CONFIG[normalized]) {
return { key: normalized, ...APP_FONT_CONFIG[normalized] };
}
return {
key: APP_FONT_DEFAULT_KEY,
...APP_FONT_CONFIG[APP_FONT_DEFAULT_KEY]
};
}
function ensureAppFontLinks(fontKey) {
const head = document.head;
if (!head) {
return;
}
document
.querySelectorAll(`style[${APP_FONT_LINK_ATTR}]`)
.forEach((styleEl) => {
if (styleEl.getAttribute(APP_FONT_LINK_ATTR) !== fontKey) {
styleEl.remove();
}
});
const config = APP_FONT_CONFIG[fontKey];
if (!config?.cssImport) {
return;
}
const existing = document.querySelector(
`style[${APP_FONT_LINK_ATTR}="${fontKey}"]`
);
if (existing) {
return;
}
const styleEl = document.createElement('style');
styleEl.setAttribute(APP_FONT_LINK_ATTR, fontKey);
styleEl.textContent = config.cssImport;
head.appendChild(styleEl);
}
function applyAppFontFamily(fontKey) {
const resolved = resolveAppFontFamily(fontKey);
const root = document.documentElement;
root.style.setProperty('--font-western-primary', resolved.cssName);
ensureAppFontLinks(resolved.key);
return resolved;
}
function changeAppThemeStyle(themeMode) {
if (themeMode === 'system') {
themeMode = systemIsDarkMode() ? 'dark' : 'light';
}
let themeConfig = THEME_CONFIG[themeMode];
if (!themeConfig) {
// fallback to system
console.error('Invalid theme mode:', themeMode);
configRepository.setString('VRCX_ThemeMode', 'system');
themeMode = systemIsDarkMode() ? 'dark' : 'light';
themeConfig = THEME_CONFIG[themeMode];
}
applyThemeFonts(themeMode, themeConfig.fontLinks);
applyThemeModeStyle(themeMode);
document.documentElement.setAttribute('data-theme', themeMode);
const shouldUseDarkClass = Boolean(themeConfig.isDark);
if (shouldUseDarkClass) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
if (themeConfig.name === 'Midnight') {
AppApi.ChangeTheme(2);
} else if (themeConfig.isDark) {
AppApi.ChangeTheme(1);
} else {
AppApi.ChangeTheme(0);
}
return { isDark: themeConfig.isDark };
// let $appThemeDarkStyle = document.getElementById('app-theme-dark-style');
// const darkThemeCssPath = `${filePathPrefix}theme.dark.css`;
// const shouldApplyDarkBase = themeConfig.isDark;
// if (shouldApplyDarkBase) {
// if (!$appThemeDarkStyle) {
// $appThemeDarkStyle = document.createElement('link');
// $appThemeDarkStyle.setAttribute('id', 'app-theme-dark-style');
// $appThemeDarkStyle.rel = 'stylesheet';
// $appThemeDarkStyle.href = darkThemeCssPath;
// document.head.insertBefore($appThemeDarkStyle, $appThemeStyle);
// } else if ($appThemeDarkStyle.href !== darkThemeCssPath) {
// $appThemeDarkStyle.href = darkThemeCssPath;
// }
// } else {
// $appThemeDarkStyle && $appThemeDarkStyle.remove();
// }
}
/**
*
* @param {object} trustColor
*/
function updateTrustColorClasses(trustColor) {
if (document.getElementById('trustColor') !== null) {
document.getElementById('trustColor').outerHTML = '';
}
const style = document.createElement('style');
style.id = 'trustColor';
style.type = 'text/css';
let newCSS = '';
for (const rank in trustColor) {
newCSS += `.x-tag-${rank} { color: ${trustColor[rank]} !important; border-color: ${trustColor[rank]} !important; } `;
}
style.innerHTML = newCSS;
document.getElementsByTagName('head')[0].appendChild(style);
}
async function refreshCustomCss() {
if (document.contains(document.getElementById('app-custom-style'))) {
document.getElementById('app-custom-style').remove();
}
const customCss = await AppApi.CustomCss();
if (customCss) {
const head = document.head;
const $appCustomStyle = document.createElement('link');
$appCustomStyle.setAttribute('id', 'app-custom-style');
$appCustomStyle.rel = 'stylesheet';
$appCustomStyle.type = 'text/css';
$appCustomStyle.href = URL.createObjectURL(
new Blob([customCss], { type: 'text/css' })
);
head.appendChild($appCustomStyle);
}
}
async function refreshCustomScript() {
if (document.contains(document.getElementById('app-custom-script'))) {
document.getElementById('app-custom-script').remove();
}
const customScript = await AppApi.CustomScript();
if (customScript) {
const head = document.head;
const $appCustomScript = document.createElement('script');
$appCustomScript.setAttribute('id', 'app-custom-script');
$appCustomScript.type = 'text/javascript';
$appCustomScript.textContent = customScript;
head.appendChild($appCustomScript);
}
}
/**
*
* @param {number} hue
* @returns {string}
*/
function HueToHex(hue) {
const appSettingsStore = useAppearanceSettingsStore();
const { isDarkMode } = storeToRefs(appSettingsStore);
// this.HSVtoRGB(hue / 65535, .8, .8);
if (isDarkMode.value) {
return HSVtoRGB(hue / 65535, 0.6, 1);
}
return HSVtoRGB(hue / 65535, 1, 0.7);
}
/**
*
* @param {number} h
* @param {number} s
* @param {number} v
* @returns {string}
*/
function HSVtoRGB(h, s, v) {
let r = 0;
let g = 0;
let b = 0;
if (arguments.length === 1) {
// @ts-ignore
s = h.s;
// @ts-ignore
v = h.v;
// @ts-ignore
h = h.h;
}
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
const red = Math.round(r * 255);
const green = Math.round(g * 255);
const blue = Math.round(b * 255);
const decColor = 0x1000000 + blue + 0x100 * green + 0x10000 * red;
return `#${decColor.toString(16).substr(1)}`;
}
function formatJsonVars(ref) {
// remove all object keys that start with $
const newRef = { ...ref };
for (const key in newRef) {
if (key.startsWith('$')) {
delete newRef[key];
}
}
// sort keys alphabetically
const sortedKeys = Object.keys(newRef).sort();
const sortedRef = {};
sortedKeys.forEach((key) => {
sortedRef[key] = newRef[key];
});
if ('displayName' in sortedRef) {
// add _hexDisplayName to top
return {
// @ts-ignore
_hexDisplayName: textToHex(sortedRef.displayName),
...sortedRef
};
}
if ('name' in sortedRef) {
// add _hexName to top
return {
// @ts-ignore
_hexName: textToHex(sortedRef.name),
...sortedRef
};
}
return sortedRef;
}
function changeHtmlLangAttribute(language) {
const htmlElement = document.documentElement;
htmlElement.setAttribute('lang', language);
}
async function getThemeMode(configRepository) {
const initThemeMode = await configRepository.getString(
'VRCX_ThemeMode',
'system'
);
let isDarkMode;
if (initThemeMode === 'light') {
isDarkMode = false;
} else if (initThemeMode === 'system') {
isDarkMode = systemIsDarkMode();
} else {
isDarkMode = true;
}
return { initThemeMode, isDarkMode };
}
function redirectToToolsTab() {
router.push({ name: 'tools' });
toast(i18n.global.t('view.tools.redirect_message'), { duration: 3000 });
}
export {
systemIsDarkMode,
changeAppThemeStyle,
useThemeColor,
applyThemeColor,
initThemeColor,
updateTrustColorClasses,
refreshCustomCss,
refreshCustomScript,
applyAppFontFamily,
HueToHex,
HSVtoRGB,
formatJsonVars,
changeHtmlLangAttribute,
getThemeMode,
redirectToToolsTab
};