mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
MatugenService: Predefined schemes dont use matugen at all.
This commit is contained in:
@@ -90,3 +90,176 @@ function hslToHex(h, s, l) {
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 0, g: 0, b: 0 };
|
||||
}
|
||||
|
||||
function rgbToHex(r, g, b) {
|
||||
return "#" + [r, g, b].map(x => {
|
||||
const hex = Math.round(Math.max(0, Math.min(255, x))).toString(16);
|
||||
return hex.length === 1 ? "0" + hex : hex;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return { h: h * 360, s: s * 100, l: l * 100 };
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) {
|
||||
h /= 360;
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
|
||||
let r, g, b;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l;
|
||||
} else {
|
||||
const hue2rgb = (p, q, t) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||
if (t < 1/2) return q;
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
|
||||
r = hue2rgb(p, q, h + 1/3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1/3);
|
||||
}
|
||||
|
||||
return { r: r * 255, g: g * 255, b: b * 255 };
|
||||
}
|
||||
|
||||
// Calculate relative luminance (WCAG standard)
|
||||
function getLuminance(hex) {
|
||||
const rgb = hexToRgb(hex);
|
||||
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(val => {
|
||||
val /= 255;
|
||||
return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
// Calculate contrast ratio between two colors
|
||||
function getContrastRatio(hex1, hex2) {
|
||||
const lum1 = getLuminance(hex1);
|
||||
const lum2 = getLuminance(hex2);
|
||||
const brightest = Math.max(lum1, lum2);
|
||||
const darkest = Math.min(lum1, lum2);
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
}
|
||||
|
||||
// Check if a color is considered "light"
|
||||
function isLightColor(hex) {
|
||||
return getLuminance(hex) > 0.5;
|
||||
}
|
||||
|
||||
// Adjust color lightness
|
||||
function adjustLightness(hex, amount) {
|
||||
const rgb = hexToRgb(hex);
|
||||
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
||||
hsl.l = Math.max(0, Math.min(100, hsl.l + amount));
|
||||
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
||||
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
||||
}
|
||||
|
||||
// Adjust color saturation
|
||||
function adjustSaturation(hex, amount) {
|
||||
const rgb = hexToRgb(hex);
|
||||
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
||||
hsl.s = Math.max(0, Math.min(100, hsl.s + amount));
|
||||
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
||||
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
||||
}
|
||||
|
||||
// Generate "on" color with proper contrast (for text/icons)
|
||||
function generateOnColor(baseColor, isDarkMode) {
|
||||
const isBaseLight = isLightColor(baseColor);
|
||||
|
||||
// If base is light, we need dark text; if base is dark, we need light text
|
||||
if (isBaseLight) {
|
||||
// Try darker variants
|
||||
let testColor = "#000000";
|
||||
if (getContrastRatio(baseColor, testColor) >= 4.5) {
|
||||
return testColor;
|
||||
}
|
||||
// Fallback to dark gray
|
||||
return "#1c1b1f";
|
||||
} else {
|
||||
// Try lighter variants
|
||||
let testColor = "#ffffff";
|
||||
if (getContrastRatio(baseColor, testColor) >= 4.5) {
|
||||
return testColor;
|
||||
}
|
||||
// Fallback to light gray
|
||||
return "#e6e1e5";
|
||||
}
|
||||
}
|
||||
|
||||
// Generate container color (lighter in light mode, darker in dark mode)
|
||||
function generateContainerColor(baseColor, isDarkMode) {
|
||||
const rgb = hexToRgb(baseColor);
|
||||
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
||||
|
||||
if (isDarkMode) {
|
||||
// In dark mode, containers are darker and more saturated
|
||||
hsl.l = Math.max(10, Math.min(30, hsl.l - 20));
|
||||
hsl.s = Math.min(100, hsl.s + 10);
|
||||
} else {
|
||||
// In light mode, containers are lighter and less saturated
|
||||
hsl.l = Math.min(90, Math.max(75, hsl.l + 30));
|
||||
hsl.s = Math.max(0, hsl.s - 10);
|
||||
}
|
||||
|
||||
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
||||
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
||||
}
|
||||
|
||||
// Generate surface variant colors
|
||||
function generateSurfaceVariant(backgroundColor, step, isDarkMode) {
|
||||
const rgb = hexToRgb(backgroundColor);
|
||||
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
||||
|
||||
if (isDarkMode) {
|
||||
// In dark mode, variants get progressively lighter
|
||||
hsl.l = Math.min(100, hsl.l + (step * 3));
|
||||
} else {
|
||||
// In light mode, variants get progressively darker
|
||||
hsl.l = Math.max(0, hsl.l - (step * 2));
|
||||
}
|
||||
|
||||
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
||||
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
||||
}
|
||||
|
||||
+146
-113
@@ -12,47 +12,6 @@ Singleton {
|
||||
|
||||
readonly property string colorsApplyScript: Quickshell.shellDir + '/Bin/colors-apply.sh'
|
||||
readonly property string dynamicConfigPath: Settings.cacheDir + "matugen.dynamic.toml"
|
||||
|
||||
readonly property var templateConfigs: ({
|
||||
"gtk": {
|
||||
"input": "gtk.css",
|
||||
"outputs": [{
|
||||
"path": "~/.config/gtk-3.0/gtk.css"
|
||||
}, {
|
||||
"path": "~/.config/gtk-4.0/gtk.css"
|
||||
}],
|
||||
"postProcess": mode => `gsettings set org.gnome.desktop.interface color-scheme prefer-${mode}\n`
|
||||
},
|
||||
"qt": {
|
||||
"input": "qtct.conf",
|
||||
"outputs": [{
|
||||
"path": "~/.config/qt5ct/colors/noctalia.conf"
|
||||
}, {
|
||||
"path": "~/.config/qt6ct/colors/noctalia.conf"
|
||||
}]
|
||||
},
|
||||
"fuzzel": {
|
||||
"input": "fuzzel.conf",
|
||||
"outputs": [{
|
||||
"path": "~/.config/fuzzel/themes/noctalia"
|
||||
}],
|
||||
"postProcess": () => `${colorsApplyScript} fuzzel\n`
|
||||
},
|
||||
"pywalfox": {
|
||||
"input": "pywalfox.json",
|
||||
"outputs": [{
|
||||
"path": "~/.cache/wal/colors.json"
|
||||
}],
|
||||
"postProcess": () => `${colorsApplyScript} pywalfox\n`
|
||||
},
|
||||
"vesktop": {
|
||||
"input": "vesktop.css",
|
||||
"outputs": [{
|
||||
"path": "~/.config/vesktop/themes/noctalia.theme.css"
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
readonly property var terminalPaths: ({
|
||||
"foot": "~/.config/foot/themes/noctalia",
|
||||
"ghostty": "~/.config/ghostty/themes/noctalia",
|
||||
@@ -64,13 +23,50 @@ Singleton {
|
||||
"Noctalia (legacy)": "Noctalia-legacy",
|
||||
"Tokyo Night": "Tokyo-Night"
|
||||
})
|
||||
readonly property var predefinedTemplateConfigs: ({
|
||||
"gtk": {
|
||||
"input": "gtk.css",
|
||||
"outputs": [{
|
||||
"path": "~/.config/gtk-3.0/gtk.css"
|
||||
}, {
|
||||
"path": "~/.config/gtk-4.0/gtk.css"
|
||||
}],
|
||||
"postProcess": mode => `gsettings set org.gnome.desktop.interface color-scheme prefer-${mode}\n`
|
||||
},
|
||||
"qt": {
|
||||
"input": "qtct.conf",
|
||||
"outputs": [{
|
||||
"path": "~/.config/qt5ct/colors/noctalia.conf"
|
||||
}, {
|
||||
"path": "~/.config/qt6ct/colors/noctalia.conf"
|
||||
}]
|
||||
},
|
||||
"fuzzel": {
|
||||
"input": "fuzzel.conf",
|
||||
"outputs": [{
|
||||
"path": "~/.config/fuzzel/themes/noctalia"
|
||||
}],
|
||||
"postProcess": () => `${colorsApplyScript} fuzzel\n`
|
||||
},
|
||||
"pywalfox": {
|
||||
"input": "pywalfox.json",
|
||||
"outputs": [{
|
||||
"path": "~/.cache/wal/colors.json"
|
||||
}],
|
||||
"postProcess": () => `${colorsApplyScript} pywalfox\n`
|
||||
},
|
||||
"vesktop": {
|
||||
"input": "vesktop.css",
|
||||
"outputs": [{
|
||||
"path": "~/.config/vesktop/themes/noctalia.theme.css"
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// ===== Lifecycle =====
|
||||
function init() {
|
||||
Logger.log("Matugen", "Service started")
|
||||
}
|
||||
|
||||
// ===== External Connections =====
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
function onWallpaperChanged(screenName, path) {
|
||||
@@ -90,7 +86,9 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Wallpaper Generation =====
|
||||
// --------------------------------------------------------------------------------
|
||||
// Wallpaper Colors Generation
|
||||
// --------------------------------------------------------------------------------
|
||||
function generateFromWallpaper() {
|
||||
Logger.log("Matugen", "Generating from wallpaper on screen:", Screen.name)
|
||||
|
||||
@@ -122,79 +120,126 @@ Singleton {
|
||||
return script + "\n"
|
||||
}
|
||||
|
||||
// ===== Predefined Scheme Generation =====
|
||||
// --------------------------------------------------------------------------------
|
||||
// Predefined Scheme Generation
|
||||
// For predefined color schemes, we bypass matugen's generation which do not gives good results.
|
||||
// Instead, we use 'sed' to apply a custom palette to the existing matugen templates.
|
||||
// --------------------------------------------------------------------------------
|
||||
function generateFromPredefinedScheme(schemeData) {
|
||||
Logger.log("Matugen", "Generating templates from predefined color scheme")
|
||||
|
||||
handleTerminalThemes()
|
||||
|
||||
const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
|
||||
const colors = schemeData[mode] || schemeData.dark || schemeData.light
|
||||
const matugenColors = buildMatugenColorObject(colors)
|
||||
const isDarkMode = Settings.data.colorSchemes.darkMode
|
||||
const colors = schemeData[isDarkMode ? "dark" : "light"]
|
||||
|
||||
const matugenColors = generatePalette(colors.mPrimary, colors.mSecondary, colors.mTertiary, colors.mSurface, isDarkMode)
|
||||
|
||||
const mode = isDarkMode ? "dark" : "light"
|
||||
const script = processAllTemplates(matugenColors, mode)
|
||||
|
||||
generateProcess.command = ["bash", "-lc", script]
|
||||
generateProcess.running = true
|
||||
}
|
||||
|
||||
function buildMatugenColorObject(colors) {
|
||||
// Helper with fallback support
|
||||
const c = (color, fallback) => ({
|
||||
"default": {
|
||||
"hex": colors[color] || colors[fallback] || "#000000"
|
||||
}
|
||||
})
|
||||
function generatePalette(primaryColor, secondaryColor, tertiaryColor, backgroundColor, isDarkMode) {
|
||||
const c = hex => ({
|
||||
"default": {
|
||||
"hex": hex
|
||||
}
|
||||
})
|
||||
|
||||
// Generate container colors
|
||||
const primaryContainer = ColorsConvert.generateContainerColor(primaryColor, isDarkMode)
|
||||
const secondaryContainer = ColorsConvert.generateContainerColor(secondaryColor, isDarkMode)
|
||||
const tertiaryContainer = ColorsConvert.generateContainerColor(tertiaryColor, isDarkMode)
|
||||
|
||||
// Generate "on" colors (for text/icons)
|
||||
const onPrimary = ColorsConvert.generateOnColor(primaryColor, isDarkMode)
|
||||
const onSecondary = ColorsConvert.generateOnColor(secondaryColor, isDarkMode)
|
||||
const onTertiary = ColorsConvert.generateOnColor(tertiaryColor, isDarkMode)
|
||||
const onBackground = ColorsConvert.generateOnColor(backgroundColor, isDarkMode)
|
||||
|
||||
const onPrimaryContainer = ColorsConvert.generateOnColor(primaryContainer, isDarkMode)
|
||||
const onSecondaryContainer = ColorsConvert.generateOnColor(secondaryContainer, isDarkMode)
|
||||
const onTertiaryContainer = ColorsConvert.generateOnColor(tertiaryContainer, isDarkMode)
|
||||
|
||||
// Generate error colors (standard red-based)
|
||||
const errorColor = isDarkMode ? "#f2b8b5" : "#ba1a1a"
|
||||
const errorContainer = isDarkMode ? "#8c1d18" : "#ffdad6"
|
||||
const onError = ColorsConvert.generateOnColor(errorColor, isDarkMode)
|
||||
const onErrorContainer = ColorsConvert.generateOnColor(errorContainer, isDarkMode)
|
||||
|
||||
// Surface is same as background in Material Design 3
|
||||
const surface = backgroundColor
|
||||
const onSurface = onBackground
|
||||
|
||||
// Generate surface variant (slightly different tone)
|
||||
const surfaceVariant = ColorsConvert.adjustLightness(backgroundColor, isDarkMode ? 5 : -3)
|
||||
const onSurfaceVariant = ColorsConvert.generateOnColor(surfaceVariant, isDarkMode)
|
||||
|
||||
// Generate surface containers (progressive elevation)
|
||||
const surfaceContainerLowest = ColorsConvert.generateSurfaceVariant(backgroundColor, 0, isDarkMode)
|
||||
const surfaceContainerLow = ColorsConvert.generateSurfaceVariant(backgroundColor, 1, isDarkMode)
|
||||
const surfaceContainer = ColorsConvert.generateSurfaceVariant(backgroundColor, 2, isDarkMode)
|
||||
const surfaceContainerHigh = ColorsConvert.generateSurfaceVariant(backgroundColor, 3, isDarkMode)
|
||||
const surfaceContainerHighest = ColorsConvert.generateSurfaceVariant(backgroundColor, 4, isDarkMode)
|
||||
|
||||
// Generate outline colors (for borders/dividers)
|
||||
const outline = isDarkMode ? "#938f99" : "#79747e"
|
||||
const outlineVariant = ColorsConvert.adjustLightness(outline, isDarkMode ? -10 : 10)
|
||||
|
||||
// Shadow is always very dark
|
||||
const shadow = "#000000"
|
||||
|
||||
return {
|
||||
"primary": c("mPrimary"),
|
||||
"on_primary": c("mOnPrimary"),
|
||||
"primary_container": c("mPrimaryContainer", "mPrimary"),
|
||||
"on_primary_container": c("mOnPrimaryContainer", "mOnPrimary"),
|
||||
"secondary": c("mSecondary"),
|
||||
"on_secondary": c("mOnSecondary"),
|
||||
"secondary_container": c("mSecondaryContainer", "mSecondary"),
|
||||
"on_secondary_container": c("mOnSecondaryContainer", "mOnSecondary"),
|
||||
"tertiary": c("mTertiary"),
|
||||
"on_tertiary": c("mOnTertiary"),
|
||||
"tertiary_container": c("mTertiaryContainer", "mTertiary"),
|
||||
"on_tertiary_container": c("mOnTertiaryContainer", "mOnTertiary"),
|
||||
"error": c("mError"),
|
||||
"on_error": c("mOnError"),
|
||||
"error_container": c("mErrorContainer", "mError"),
|
||||
"on_error_container": c("mOnErrorContainer", "mOnError"),
|
||||
"background": c("mBackground", "mSurface"),
|
||||
"on_background": c("mOnBackground", "mOnSurface"),
|
||||
"surface": c("mSurface"),
|
||||
"on_surface": c("mOnSurface"),
|
||||
"surface_variant": c("mSurfaceVariant", "mSurface"),
|
||||
"on_surface_variant": c("mOnSurfaceVariant", "mOnSurface"),
|
||||
"surface_container_lowest": c("mSurfaceContainerLowest", "mSurface"),
|
||||
"surface_container_low": c("mSurfaceContainerLow", "mSurface"),
|
||||
"surface_container": c("mSurfaceContainer", "mSurfaceVariant"),
|
||||
"surface_container_high": c("mSurfaceContainerHigh", "mSurfaceVariant"),
|
||||
"surface_container_highest": c("mSurfaceContainerHighest", "mOutline"),
|
||||
"outline": c("mOutline"),
|
||||
"outline_variant": c("mOutlineVariant", "mOutline"),
|
||||
"shadow": c("mShadow")
|
||||
"primary": c(primaryColor),
|
||||
"on_primary": c(onPrimary),
|
||||
"primary_container": c(primaryContainer),
|
||||
"on_primary_container": c(onPrimaryContainer),
|
||||
"secondary": c(secondaryColor),
|
||||
"on_secondary": c(onSecondary),
|
||||
"secondary_container": c(secondaryContainer),
|
||||
"on_secondary_container": c(onSecondaryContainer),
|
||||
"tertiary": c(tertiaryColor),
|
||||
"on_tertiary": c(onTertiary),
|
||||
"tertiary_container": c(tertiaryContainer),
|
||||
"on_tertiary_container": c(onTertiaryContainer),
|
||||
"error": c(errorColor),
|
||||
"on_error": c(onError),
|
||||
"error_container": c(errorContainer),
|
||||
"on_error_container": c(onErrorContainer),
|
||||
"background": c(backgroundColor),
|
||||
"on_background": c(onBackground),
|
||||
"surface": c(surface),
|
||||
"on_surface": c(onSurface),
|
||||
"surface_variant": c(surfaceVariant),
|
||||
"on_surface_variant": c(onSurfaceVariant),
|
||||
"surface_container_lowest": c(surfaceContainerLowest),
|
||||
"surface_container_low": c(surfaceContainerLow),
|
||||
"surface_container": c(surfaceContainer),
|
||||
"surface_container_high": c(surfaceContainerHigh),
|
||||
"surface_container_highest": c(surfaceContainerHighest),
|
||||
"outline": c(outline),
|
||||
"outline_variant": c(outlineVariant),
|
||||
"shadow": c(shadow)
|
||||
}
|
||||
}
|
||||
|
||||
function processAllTemplates(colors, mode) {
|
||||
let script = ""
|
||||
const homeDir = Quickshell.env("HOME")
|
||||
|
||||
Object.keys(templateConfigs).forEach(appName => {
|
||||
if (Settings.data.templates[appName]) {
|
||||
script += processTemplate(appName, colors, mode, homeDir)
|
||||
}
|
||||
})
|
||||
Object.keys(predefinedTemplateConfigs).forEach(appName => {
|
||||
if (Settings.data.templates[appName]) {
|
||||
script += processTemplate(appName, colors, mode, homeDir)
|
||||
}
|
||||
})
|
||||
|
||||
return script
|
||||
}
|
||||
|
||||
function processTemplate(appName, colors, mode, homeDir) {
|
||||
const config = templateConfigs[appName]
|
||||
const config = predefinedTemplateConfigs[appName]
|
||||
const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${config.input}`
|
||||
let script = ""
|
||||
|
||||
@@ -224,7 +269,9 @@ Singleton {
|
||||
return script
|
||||
}
|
||||
|
||||
// ===== Terminal Themes =====
|
||||
// --------------------------------------------------------------------------------
|
||||
// Terminal Themes
|
||||
// --------------------------------------------------------------------------------
|
||||
function handleTerminalThemes() {
|
||||
const commands = []
|
||||
|
||||
@@ -256,7 +303,9 @@ Singleton {
|
||||
return `${Quickshell.shellDir}/Assets/ColorScheme/${colorScheme}/terminal/${terminal}/${colorScheme}-${mode}${extension}`
|
||||
}
|
||||
|
||||
// ===== User Templates =====
|
||||
// --------------------------------------------------------------------------------
|
||||
// User Templates
|
||||
// --------------------------------------------------------------------------------
|
||||
function buildUserTemplateCommand(input, mode) {
|
||||
if (!Settings.data.templates.enableUserTemplates) {
|
||||
return ""
|
||||
@@ -275,25 +324,9 @@ Singleton {
|
||||
return (Quickshell.env("HOME") + "/.config/matugen/config.toml").replace(/'/g, "'\\''")
|
||||
}
|
||||
|
||||
// ===== Utilities =====
|
||||
function selectVibrantColor(schemeData, mode) {
|
||||
const colors = [schemeData[mode]["mPrimary"], schemeData[mode]["mSecondary"], schemeData[mode]["mTertiary"]]
|
||||
|
||||
let bestScore = 0
|
||||
let bestIndex = 0
|
||||
|
||||
colors.forEach((color, i) => {
|
||||
const hsl = ColorsConvert.hexToHSL(color)
|
||||
if (hsl.s > bestScore) {
|
||||
bestScore = hsl.s
|
||||
bestIndex = i
|
||||
}
|
||||
})
|
||||
|
||||
return colors[bestIndex]
|
||||
}
|
||||
|
||||
// ===== Processes =====
|
||||
// --------------------------------------------------------------------------------
|
||||
// Processes
|
||||
// --------------------------------------------------------------------------------
|
||||
Process {
|
||||
id: generateProcess
|
||||
workingDirectory: Quickshell.shellDir
|
||||
|
||||
Reference in New Issue
Block a user