Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4188d68457 | ||
|
|
6391a4e7e2 | ||
|
|
41048638a2 | ||
|
|
9c0e3e7937 | ||
|
|
fe3dd8edb5 | ||
|
|
0d46089f9a | ||
|
|
7420c10a58 | ||
|
|
e40c84f31d | ||
|
|
994fc062cf | ||
|
|
e7681f6c79 | ||
|
|
19053339d9 | ||
|
|
b4e16c83e2 | ||
|
|
56cc89b521 | ||
|
|
1eab314b17 | ||
|
|
ec21336d45 | ||
|
|
e86e9b46b3 | ||
|
|
9b729b3d25 | ||
|
|
3c973e21f2 | ||
|
|
830e2f0a5b | ||
|
|
1620477a1c | ||
|
|
92b9a5218d | ||
|
|
9ed0d5ccec | ||
|
|
a6d1727205 | ||
|
|
3c3982464f | ||
|
|
bef73ff530 | ||
|
|
4d31c0b1de | ||
|
|
a5f28c21e4 | ||
|
|
c93ead7474 | ||
|
|
36880493cb | ||
|
|
e8518de054 | ||
|
|
b99e13e667 | ||
|
|
2518cf36d0 |
3
.gitignore
vendored
@@ -81,6 +81,3 @@ vite.config.local-dev.ts.timestamp-*
|
||||
|
||||
# VSCode addon
|
||||
.favorites.json
|
||||
|
||||
# Affinity
|
||||
*.af~lock~
|
||||
|
||||
15
CHANGELOG.md
@@ -1,18 +1,3 @@
|
||||
## Unreleased
|
||||
|
||||
### General
|
||||
-
|
||||
|
||||
### Client
|
||||
- Enhance: テーマのプレビュー時、リロードせずにもとのテーマに戻せるように
|
||||
- Fix: テーマエディター使用時に、最初の変更のみ適用される問題を修正
|
||||
- Fix: テーマのプレビュー時、既存のテーマとIDが被っている場合にプレビューできない問題を修正
|
||||
- Fix: テーマのインストールエラーの表示を改善
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
|
||||
## 2026.5.1
|
||||
|
||||
### General
|
||||
|
||||
@@ -1409,8 +1409,6 @@ presets: "プリセット"
|
||||
zeroPadding: "ゼロ埋め"
|
||||
nothingToConfigure: "設定項目はありません"
|
||||
viewRenotedChannel: "リノート先のチャンネルを見る"
|
||||
previewingTheme: "テーマのプレビュー中"
|
||||
previewingThemeRestore: "元に戻す"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
@@ -3556,17 +3554,3 @@ _qr:
|
||||
scanFile: "端末の画像をスキャン"
|
||||
raw: "テキスト"
|
||||
mfm: "MFM"
|
||||
|
||||
_room:
|
||||
snapToGrid: "グリッドにスナップ"
|
||||
gridScale: "グリッドサイズ"
|
||||
thereAreUnsavedChanges: "未保存の変更があります"
|
||||
revertAllChangesConfirmation: "全ての変更を取り消し、部屋を最後に保存した状態まで戻しますか?"
|
||||
graphicsQuality: "グラフィックの品質"
|
||||
frameRate: "フレームレート"
|
||||
resolution: "解像度"
|
||||
yourDeviceNotSupported_title: "お使いのデバイスはMisskeyRoomをサポートしていません。"
|
||||
yourDeviceNotSupported_description: "MisskeyRoomを動作させるには、WebGPUをサポートするデバイスが必要です。"
|
||||
failedToInitialize: "初期化に失敗しました"
|
||||
crushed_description: "バグ、またはデバイスのリソース不足の可能性が考えられます。"
|
||||
antialiasing: "アンチエイリアス"
|
||||
|
||||
@@ -28,7 +28,7 @@ import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
|
||||
import { serverContext } from '@/server-context.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
|
||||
console.log('Misskey Embed');
|
||||
|
||||
|
||||
@@ -5,10 +5,26 @@
|
||||
|
||||
// TODO: (可能な部分を)sharedに抽出して frontend と共通化
|
||||
|
||||
import tinycolor from 'tinycolor2';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import { compile } from '@@/js/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import type { BundledTheme } from 'shiki/themes';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
name: string;
|
||||
author: string;
|
||||
desc?: string;
|
||||
base?: 'dark' | 'light';
|
||||
props: Record<string, string>;
|
||||
codeHighlighter?: {
|
||||
base: BundledTheme;
|
||||
overrides?: Record<string, any>;
|
||||
} | {
|
||||
base: '_none_';
|
||||
overrides: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
let timeout: number | null = null;
|
||||
|
||||
@@ -16,7 +32,7 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
|
||||
return typeof theme === 'object' && theme !== null && 'id' in theme && 'name' in theme && 'author' in theme && 'props' in theme;
|
||||
}
|
||||
|
||||
export function applyTheme(theme: Theme) {
|
||||
export function applyTheme(theme: Theme, persist = true) {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
@@ -52,3 +68,48 @@ export function applyTheme(theme: Theme) {
|
||||
|
||||
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
|
||||
}
|
||||
|
||||
function compile(theme: Theme): Record<string, string> {
|
||||
function getColor(val: string): tinycolor.Instance {
|
||||
if (val[0] === '@') { // ref (prop)
|
||||
return getColor(theme.props[val.substring(1)]);
|
||||
} else if (val[0] === '$') { // ref (const)
|
||||
return getColor(theme.props[val]);
|
||||
} else if (val[0] === ':') { // func
|
||||
const parts = val.split('<');
|
||||
const funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
const color = getColor(parts.join('<'));
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other case
|
||||
return tinycolor(val);
|
||||
}
|
||||
|
||||
const props = {};
|
||||
|
||||
for (const [k, v] of Object.entries(theme.props)) {
|
||||
if (k.startsWith('$')) continue; // ignore const
|
||||
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function genValue(c: tinycolor.Instance): string {
|
||||
return c.toRgbString();
|
||||
}
|
||||
|
||||
13
packages/frontend-shared/@types/theme.d.ts
vendored
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare module '@@/themes/*.json5' {
|
||||
import { Theme } from '@@/js/theme.js';
|
||||
|
||||
const theme: Theme;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default theme;
|
||||
}
|
||||
109
packages/frontend-shared/build.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as esbuild from 'esbuild';
|
||||
import { build } from 'esbuild';
|
||||
import { execa } from 'execa';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
|
||||
|
||||
const entryPoints = fs.globSync('./js/**/**.{ts,tsx}');
|
||||
|
||||
/** @type {import('esbuild').BuildOptions} */
|
||||
const options = {
|
||||
entryPoints,
|
||||
minify: process.env.NODE_ENV === 'production',
|
||||
outdir: './js-built',
|
||||
target: 'es2022',
|
||||
platform: 'browser',
|
||||
format: 'esm',
|
||||
sourcemap: 'linked',
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
|
||||
|
||||
// js-built配下をすべて削除する
|
||||
if (!args.includes('--no-clean')) {
|
||||
fs.rmSync('./js-built', { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (args.includes('--watch')) {
|
||||
await watchSrc();
|
||||
} else {
|
||||
await buildSrc();
|
||||
}
|
||||
|
||||
async function buildSrc() {
|
||||
console.log(`[${_package.name}] start building...`);
|
||||
|
||||
await build(options)
|
||||
.then(() => {
|
||||
console.log(`[${_package.name}] build succeeded.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
process.stderr.write(err.stderr);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
|
||||
} else {
|
||||
await buildDts();
|
||||
}
|
||||
|
||||
fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json');
|
||||
|
||||
console.log(`[${_package.name}] finish building.`);
|
||||
}
|
||||
|
||||
function buildDts() {
|
||||
return execa(
|
||||
'tsgo',
|
||||
[
|
||||
'--project', 'tsconfig.json',
|
||||
'--outDir', 'js-built',
|
||||
'--declaration', 'true',
|
||||
'--emitDeclarationOnly', 'true',
|
||||
],
|
||||
{
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function watchSrc() {
|
||||
const plugins = [{
|
||||
name: 'gen-dts',
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
console.log(`[${_package.name}] detect changed...`);
|
||||
});
|
||||
build.onEnd(async result => {
|
||||
if (result.errors.length > 0) {
|
||||
console.error(`[${_package.name}] watch build failed:`, result);
|
||||
return;
|
||||
}
|
||||
await buildDts();
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
console.log(`[${_package.name}] start watching...`);
|
||||
|
||||
const context = await esbuild.context({ ...options, plugins });
|
||||
await context.watch();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
process.on('SIGHUP', resolve);
|
||||
process.on('SIGINT', resolve);
|
||||
process.on('SIGTERM', resolve);
|
||||
process.on('uncaughtException', reject);
|
||||
process.on('exit', resolve);
|
||||
}).finally(async () => {
|
||||
await context.dispose();
|
||||
console.log(`[${_package.name}] finish watching.`);
|
||||
});
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import tinycolor from 'tinycolor2';
|
||||
import JSON5 from 'json5';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import type { BundledTheme } from 'shiki/themes';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
name: string;
|
||||
author: string;
|
||||
desc?: string;
|
||||
base?: 'dark' | 'light';
|
||||
kind?: 'dark' | 'light'; // legacy
|
||||
props: Record<string, string>;
|
||||
codeHighlighter?: {
|
||||
base: BundledTheme;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
overrides?: Record<string, any>;
|
||||
} | {
|
||||
base: '_none_';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
overrides: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
export type CompiledTheme = Record<string, string>;
|
||||
|
||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||
|
||||
export const getBuiltinThemes = () => Promise.all(
|
||||
[
|
||||
'l-light',
|
||||
'l-coffee',
|
||||
'l-apricot',
|
||||
'l-rainy',
|
||||
'l-botanical',
|
||||
'l-vivid',
|
||||
'l-cherry',
|
||||
'l-sushi',
|
||||
'l-u0',
|
||||
|
||||
'd-dark',
|
||||
'd-persimmon',
|
||||
'd-astro',
|
||||
'd-future',
|
||||
'd-botanical',
|
||||
'd-green-lime',
|
||||
'd-green-orange',
|
||||
'd-cherry',
|
||||
'd-ice',
|
||||
'd-u0',
|
||||
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
|
||||
export function compile(theme: Theme): CompiledTheme {
|
||||
function getColor(val: string): tinycolor.Instance {
|
||||
if (val[0] === '@') { // ref (prop)
|
||||
return getColor(theme.props[val.substring(1)]);
|
||||
} else if (val[0] === '$') { // ref (const)
|
||||
return getColor(theme.props[val]);
|
||||
} else if (val[0] === ':') { // func
|
||||
const parts = val.split('<');
|
||||
const funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
const color = getColor(parts.join('<'));
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other case
|
||||
return tinycolor(val);
|
||||
}
|
||||
|
||||
const props = {} as CompiledTheme;
|
||||
|
||||
for (const [k, v] of Object.entries(theme.props)) {
|
||||
if (k.startsWith('$')) continue; // ignore const
|
||||
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function genValue(c: tinycolor.Instance): string {
|
||||
return c.toRgbString();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function validateTheme(theme: Record<string, any>): boolean {
|
||||
if (theme.id == null || typeof theme.id !== 'string') return false;
|
||||
if (theme.name == null || typeof theme.name !== 'string') return false;
|
||||
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
|
||||
if (theme.props == null || typeof theme.props !== 'object') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function parseThemeCode(code: string): Theme {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (_) {
|
||||
throw new Error('Failed to parse theme json');
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
throw new Error('This theme is invaild');
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
@@ -1,18 +1,32 @@
|
||||
{
|
||||
"name": "frontend-shared",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "./js-built/index.js",
|
||||
"types": "./js-built/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./js-built/index.js",
|
||||
"types": "./js-built/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./js-built/*",
|
||||
"types": "./js-built/*"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ./build.js",
|
||||
"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
|
||||
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.12.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"esbuild": "0.28.0",
|
||||
"eslint-plugin-vue": "10.9.0",
|
||||
"nodemon": "3.1.14",
|
||||
"vue-eslint-parser": "10.4.0"
|
||||
},
|
||||
"files": [
|
||||
@@ -20,10 +34,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"json5": "2.2.3",
|
||||
"misskey-js": "workspace:*",
|
||||
"shiki": "4.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"vue": "3.5.33"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ let moduleInitialized = false;
|
||||
let unobserve = () => {};
|
||||
let misskeyOS = null;
|
||||
|
||||
function loadTheme(themeMaganer: typeof import('../src/theme')['themeManager']) {
|
||||
function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
|
||||
unobserve();
|
||||
const theme = themes[window.document.documentElement.dataset.misskeyTheme];
|
||||
if (theme) {
|
||||
themeMaganer.updateTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
|
||||
applyTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
|
||||
} else {
|
||||
themeMaganer.updateTheme(themes['l-light']);
|
||||
applyTheme(themes['l-light']);
|
||||
}
|
||||
const observer = new MutationObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
@@ -34,7 +34,7 @@ function loadTheme(themeMaganer: typeof import('../src/theme')['themeManager'])
|
||||
const target = entry.target as HTMLElement;
|
||||
const theme = themes[target.dataset.misskeyTheme];
|
||||
if (theme) {
|
||||
themeMaganer.updateTheme(themes[target.dataset.misskeyTheme]);
|
||||
applyTheme(themes[target.dataset.misskeyTheme]);
|
||||
} else {
|
||||
target.removeAttribute('style');
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 879 KiB |
|
Before Width: | Height: | Size: 626 KiB |
@@ -1 +0,0 @@
|
||||
これらのサムネイルはdev buildでRoomのカタログダイアログを表示し家具を選択した状態でブラウザのコンソールで`takeScreenshot();`を叩くと生成・ダウンロードできます
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 33 KiB |