Add bulk image search on Checkup page and apply image fixes.
Auto-queue Google-family searches for filtered rows with Fix-it support; include corrected painting images from checkup workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -187,6 +187,91 @@
|
||||
color: #9ec5e8;
|
||||
}
|
||||
|
||||
.checkup-thumb-search {
|
||||
border-color: rgba(232, 160, 64, 0.35);
|
||||
}
|
||||
|
||||
.checkup-thumb-loading {
|
||||
color: rgba(201, 169, 110, 0.55);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.checkup-thumb-badge-search {
|
||||
border-color: rgba(232, 160, 64, 0.45);
|
||||
color: #e8a040;
|
||||
max-width: calc(100% - 12px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkup-retry-btn {
|
||||
margin-top: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid rgba(201, 169, 110, 0.35);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #c9a96e;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkup-retry-btn:hover {
|
||||
border-color: #c9a96e;
|
||||
color: #e8d5b5;
|
||||
}
|
||||
|
||||
.checkup-fix-cell-wrap {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.checkup-fix-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.checkup-fix-btn {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e8a040;
|
||||
border-radius: 6px;
|
||||
background: rgba(232, 160, 64, 0.15);
|
||||
color: #e8a040;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkup-fix-btn:hover:not(:disabled) {
|
||||
background: rgba(232, 160, 64, 0.28);
|
||||
}
|
||||
|
||||
.checkup-fix-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
border-color: rgba(201, 169, 110, 0.25);
|
||||
color: rgba(201, 169, 110, 0.45);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.checkup-fix-error {
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
color: #ff8a80;
|
||||
line-height: 1.3;
|
||||
max-width: 110px;
|
||||
}
|
||||
|
||||
.checkup-fix-hint {
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
color: rgba(201, 169, 110, 0.55);
|
||||
line-height: 1.3;
|
||||
max-width: 110px;
|
||||
}
|
||||
|
||||
.checkup-table tbody tr:hover {
|
||||
background: rgba(201, 169, 110, 0.06);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { api, type PaintingCheckupRow } from '../api/client';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
api,
|
||||
debugImageProxyUrl,
|
||||
type DebugImageSearchResult,
|
||||
type PaintingCheckupRow,
|
||||
} from '../api/client';
|
||||
import './CheckupPage.css';
|
||||
|
||||
interface Props {
|
||||
@@ -7,27 +12,43 @@ interface Props {
|
||||
onOpenPainting?: (paintingId: number) => void;
|
||||
}
|
||||
|
||||
function previewSrc(row: PaintingCheckupRow, kind: 'gallery' | 'detail'): string | null {
|
||||
type RowSearchState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'done'; result: DebugImageSearchResult }
|
||||
| { status: 'error' };
|
||||
|
||||
const SEARCH_CONCURRENCY = 3;
|
||||
const FILTER_SEARCH_DEBOUNCE_MS = 350;
|
||||
|
||||
function previewSrc(
|
||||
row: PaintingCheckupRow,
|
||||
kind: 'gallery' | 'detail',
|
||||
version: number
|
||||
): string | null {
|
||||
const bust = version > 0 ? `?v=${version}` : '';
|
||||
if (kind === 'gallery') {
|
||||
const file = row.gallery_preview ?? row.gallery_file;
|
||||
return file ? `/images/${file}` : null;
|
||||
return file ? `/images/${file}${bust}` : null;
|
||||
}
|
||||
if (row.detail_on_demand) {
|
||||
return `/api/paintings/${row.id}/image?size=thumb`;
|
||||
return `/api/paintings/${row.id}/image?size=thumb${version > 0 ? `&v=${version}` : ''}`;
|
||||
}
|
||||
const file = row.detail_preview ?? row.detail_file;
|
||||
return file ? `/images/${file}` : null;
|
||||
return file ? `/images/${file}${bust}` : null;
|
||||
}
|
||||
|
||||
function ImagePreviewCell({
|
||||
row,
|
||||
kind,
|
||||
imageVersion,
|
||||
}: {
|
||||
row: PaintingCheckupRow;
|
||||
kind: 'gallery' | 'detail';
|
||||
imageVersion: number;
|
||||
}) {
|
||||
const path = kind === 'gallery' ? row.gallery_file : row.detail_file;
|
||||
const src = previewSrc(row, kind);
|
||||
const src = previewSrc(row, kind, imageVersion);
|
||||
const exists =
|
||||
kind === 'gallery' ? row.gallery_file_exists : row.detail_file_exists;
|
||||
const missing = path && exists === false;
|
||||
@@ -63,11 +84,123 @@ function ImagePreviewCell({
|
||||
);
|
||||
}
|
||||
|
||||
function SearchPreviewCell({
|
||||
searchState,
|
||||
onRetry,
|
||||
}: {
|
||||
searchState: RowSearchState;
|
||||
onRetry: () => void;
|
||||
}) {
|
||||
if (searchState.status === 'loading' || searchState.status === 'idle') {
|
||||
return (
|
||||
<div className="checkup-thumb checkup-thumb-search checkup-thumb-loading">
|
||||
<span>{searchState.status === 'loading' ? 'Searching…' : 'Queued…'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (searchState.status === 'error') {
|
||||
return (
|
||||
<div className="checkup-thumb checkup-thumb-search checkup-thumb-empty">
|
||||
<span>Search failed</span>
|
||||
<button type="button" className="checkup-retry-btn" onClick={onRetry}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { result } = searchState;
|
||||
if (!result.imageUrl) {
|
||||
return (
|
||||
<div className="checkup-thumb checkup-thumb-search checkup-thumb-empty">
|
||||
<span>No result</span>
|
||||
<small title={result.query}>{result.sourceLabel ?? result.source}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="checkup-thumb checkup-thumb-search" title={result.query}>
|
||||
<img
|
||||
src={debugImageProxyUrl(result.imageUrl)}
|
||||
alt={`Search: ${result.query}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = '/placeholder-art.svg';
|
||||
}}
|
||||
/>
|
||||
<span className="checkup-thumb-badge checkup-thumb-badge-search">
|
||||
{result.sourceLabel ?? 'Search'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FixCell({
|
||||
searchState,
|
||||
fixing,
|
||||
fixError,
|
||||
onFix,
|
||||
}: {
|
||||
searchState: RowSearchState;
|
||||
fixing: boolean;
|
||||
fixError: string | null;
|
||||
onFix: () => void;
|
||||
}) {
|
||||
const canFix =
|
||||
searchState.status === 'done' && !!searchState.result.imageUrl && !fixing;
|
||||
|
||||
return (
|
||||
<div className="checkup-fix-cell">
|
||||
<button
|
||||
type="button"
|
||||
className="checkup-fix-btn"
|
||||
onClick={onFix}
|
||||
disabled={!canFix}
|
||||
title={
|
||||
searchState.status === 'done' && searchState.result.imageUrl
|
||||
? 'Replace gallery and detail images with search result'
|
||||
: 'Wait for search result'
|
||||
}
|
||||
>
|
||||
{fixing ? 'Replacing…' : 'Fix it'}
|
||||
</button>
|
||||
{fixError && <p className="checkup-fix-error">{fixError}</p>}
|
||||
{(searchState.status === 'loading' || searchState.status === 'idle') && (
|
||||
<p className="checkup-fix-hint">Waiting for search…</p>
|
||||
)}
|
||||
{searchState.status === 'done' && !searchState.result.imageUrl && (
|
||||
<p className="checkup-fix-hint">No image to apply</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function needsSearch(state: RowSearchState | undefined): boolean {
|
||||
return !state || state.status === 'idle' || state.status === 'error';
|
||||
}
|
||||
|
||||
export default function CheckupPage({ onBack, onOpenPainting }: Props) {
|
||||
const [rows, setRows] = useState<PaintingCheckupRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [searchById, setSearchById] = useState<Record<number, RowSearchState>>({});
|
||||
const [imageVersionById, setImageVersionById] = useState<Record<number, number>>({});
|
||||
const [fixingId, setFixingId] = useState<number | null>(null);
|
||||
const [fixErrorById, setFixErrorById] = useState<Record<number, string>>({});
|
||||
const [queuePending, setQueuePending] = useState(0);
|
||||
|
||||
const searchByIdRef = useRef(searchById);
|
||||
const queueRef = useRef<number[]>([]);
|
||||
const activeRef = useRef(0);
|
||||
const queuedSetRef = useRef(new Set<number>());
|
||||
|
||||
useEffect(() => {
|
||||
searchByIdRef.current = searchById;
|
||||
}, [searchById]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -90,6 +223,114 @@ export default function CheckupPage({ onBack, onOpenPainting }: Props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateQueuePending = useCallback(() => {
|
||||
setQueuePending(queueRef.current.length + activeRef.current);
|
||||
}, []);
|
||||
|
||||
const runSearch = useCallback(
|
||||
(rowId: number) => {
|
||||
activeRef.current += 1;
|
||||
updateQueuePending();
|
||||
setSearchById((prev) => ({ ...prev, [rowId]: { status: 'loading' } }));
|
||||
|
||||
api.getPaintingDebugImageSearch(rowId)
|
||||
.then((result) => {
|
||||
setSearchById((prev) => ({ ...prev, [rowId]: { status: 'done', result } }));
|
||||
})
|
||||
.catch(() => {
|
||||
queuedSetRef.current.delete(rowId);
|
||||
setSearchById((prev) => ({ ...prev, [rowId]: { status: 'error' } }));
|
||||
})
|
||||
.finally(() => {
|
||||
activeRef.current = Math.max(0, activeRef.current - 1);
|
||||
updateQueuePending();
|
||||
pumpQueueRef.current();
|
||||
});
|
||||
},
|
||||
[updateQueuePending]
|
||||
);
|
||||
|
||||
const pumpQueue = useCallback(() => {
|
||||
while (activeRef.current < SEARCH_CONCURRENCY && queueRef.current.length > 0) {
|
||||
const rowId = queueRef.current.shift()!;
|
||||
runSearch(rowId);
|
||||
}
|
||||
updateQueuePending();
|
||||
}, [runSearch, updateQueuePending]);
|
||||
|
||||
const pumpQueueRef = useRef(pumpQueue);
|
||||
useEffect(() => {
|
||||
pumpQueueRef.current = pumpQueue;
|
||||
}, [pumpQueue]);
|
||||
|
||||
const enqueueSearch = useCallback(
|
||||
(rowId: number) => {
|
||||
const state = searchByIdRef.current[rowId];
|
||||
if (!needsSearch(state)) return;
|
||||
if (queuedSetRef.current.has(rowId)) return;
|
||||
|
||||
queuedSetRef.current.add(rowId);
|
||||
setSearchById((prev) => ({ ...prev, [rowId]: { status: 'idle' } }));
|
||||
queueRef.current.push(rowId);
|
||||
updateQueuePending();
|
||||
pumpQueueRef.current();
|
||||
},
|
||||
[updateQueuePending]
|
||||
);
|
||||
|
||||
const retrySearch = useCallback(
|
||||
(rowId: number) => {
|
||||
queuedSetRef.current.delete(rowId);
|
||||
queueRef.current = queueRef.current.filter((id) => id !== rowId);
|
||||
setSearchById((prev) => ({ ...prev, [rowId]: { status: 'idle' } }));
|
||||
enqueueSearch(rowId);
|
||||
},
|
||||
[enqueueSearch]
|
||||
);
|
||||
|
||||
const handleFix = useCallback(
|
||||
async (row: PaintingCheckupRow) => {
|
||||
const searchState = searchById[row.id];
|
||||
if (searchState?.status !== 'done' || !searchState.result.imageUrl || fixingId) return;
|
||||
|
||||
setFixingId(row.id);
|
||||
setFixErrorById((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[row.id];
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
const updated = await api.fixPaintingImage(row.id, searchState.result.imageUrl);
|
||||
setImageVersionById((prev) => ({ ...prev, [row.id]: (prev[row.id] ?? 0) + 1 }));
|
||||
setRows((list) =>
|
||||
list.map((r) =>
|
||||
r.id === row.id
|
||||
? {
|
||||
...r,
|
||||
gallery_file: updated.imagePath,
|
||||
detail_file: updated.imagePath,
|
||||
gallery_preview: updated.thumbnailPath,
|
||||
detail_preview: updated.thumbnailPath,
|
||||
gallery_file_exists: true,
|
||||
detail_file_exists: true,
|
||||
detail_on_demand: false,
|
||||
}
|
||||
: r
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
setFixErrorById((prev) => ({
|
||||
...prev,
|
||||
[row.id]: err instanceof Error ? err.message : 'Could not replace image.',
|
||||
}));
|
||||
} finally {
|
||||
setFixingId(null);
|
||||
}
|
||||
},
|
||||
[fixingId, searchById]
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = filter.trim().toLowerCase();
|
||||
if (!q) return rows;
|
||||
@@ -103,13 +344,36 @@ export default function CheckupPage({ onBack, onOpenPainting }: Props) {
|
||||
);
|
||||
}, [rows, filter]);
|
||||
|
||||
const filteredIds = useMemo(() => filtered.map((row) => row.id), [filtered]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
for (const rowId of filteredIds) {
|
||||
enqueueSearch(rowId);
|
||||
}
|
||||
}, FILTER_SEARCH_DEBOUNCE_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [filteredIds, loading, enqueueSearch]);
|
||||
|
||||
const searchStats = useMemo(() => {
|
||||
let done = 0;
|
||||
let failed = 0;
|
||||
for (const row of filtered) {
|
||||
const state = searchById[row.id];
|
||||
if (state?.status === 'done') done += 1;
|
||||
if (state?.status === 'error') failed += 1;
|
||||
}
|
||||
return { done, failed };
|
||||
}, [filtered, searchById]);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const noGallery = rows.filter((r) => !r.gallery_file).length;
|
||||
const onDemand = rows.filter((r) => r.detail_on_demand).length;
|
||||
const missingGallery = rows.filter((r) => r.gallery_file && !r.gallery_file_exists).length;
|
||||
const missingDetail = rows.filter(
|
||||
(r) => r.detail_file_exists === false
|
||||
).length;
|
||||
const missingDetail = rows.filter((r) => r.detail_file_exists === false).length;
|
||||
return { noGallery, onDemand, missingGallery, missingDetail };
|
||||
}, [rows]);
|
||||
|
||||
@@ -121,7 +385,9 @@ export default function CheckupPage({ onBack, onOpenPainting }: Props) {
|
||||
</button>
|
||||
<div className="checkup-title-block">
|
||||
<h1>Painting checkup</h1>
|
||||
<p>Compare image files used in the 3D gallery vs painting detail view.</p>
|
||||
<p>
|
||||
Compare gallery vs detail images; filtered rows are searched automatically.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -135,6 +401,11 @@ export default function CheckupPage({ onBack, onOpenPainting }: Props) {
|
||||
/>
|
||||
<div className="checkup-stats">
|
||||
<span>{filtered.length} shown</span>
|
||||
<span>
|
||||
search {searchStats.done}/{filtered.length} done
|
||||
{queuePending > 0 ? ` · ${queuePending} active` : ''}
|
||||
</span>
|
||||
{searchStats.failed > 0 && <span>{searchStats.failed} search failed</span>}
|
||||
<span>{stats.noGallery} no gallery file</span>
|
||||
<span>{stats.onDemand} detail on-demand</span>
|
||||
<span>{stats.missingGallery} gallery missing</span>
|
||||
@@ -156,10 +427,14 @@ export default function CheckupPage({ onBack, onOpenPainting }: Props) {
|
||||
<th>Year</th>
|
||||
<th>Gallery</th>
|
||||
<th>Detail</th>
|
||||
<th>Search</th>
|
||||
<th>Fix</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((row) => (
|
||||
{filtered.map((row) => {
|
||||
const searchState = searchById[row.id] ?? { status: 'idle' as const };
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td>
|
||||
{onOpenPainting ? (
|
||||
@@ -177,13 +452,36 @@ export default function CheckupPage({ onBack, onOpenPainting }: Props) {
|
||||
<td>{row.artist_name}</td>
|
||||
<td className="checkup-year">{row.year ?? '—'}</td>
|
||||
<td className="checkup-thumb-cell">
|
||||
<ImagePreviewCell row={row} kind="gallery" />
|
||||
<ImagePreviewCell
|
||||
row={row}
|
||||
kind="gallery"
|
||||
imageVersion={imageVersionById[row.id] ?? 0}
|
||||
/>
|
||||
</td>
|
||||
<td className="checkup-thumb-cell">
|
||||
<ImagePreviewCell row={row} kind="detail" />
|
||||
<ImagePreviewCell
|
||||
row={row}
|
||||
kind="detail"
|
||||
imageVersion={imageVersionById[row.id] ?? 0}
|
||||
/>
|
||||
</td>
|
||||
<td className="checkup-thumb-cell">
|
||||
<SearchPreviewCell
|
||||
searchState={searchState}
|
||||
onRetry={() => retrySearch(row.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="checkup-fix-cell-wrap">
|
||||
<FixCell
|
||||
searchState={searchState}
|
||||
fixing={fixingId === row.id}
|
||||
fixError={fixErrorById[row.id] ?? null}
|
||||
onFix={() => handleFix(row)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 564 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 516 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 312 KiB After Width: | Height: | Size: 542 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 429 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 578 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 576 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 1002 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 598 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 598 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 513 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 2.9 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 567 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 344 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 940 KiB After Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 940 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 457 KiB |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 507 KiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 549 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 684 KiB After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 710 KiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 427 KiB |
|
Before Width: | Height: | Size: 586 KiB After Width: | Height: | Size: 546 KiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 981 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 981 KiB After Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 981 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 981 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 18 MiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 975 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 827 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 287 KiB |
|
Before Width: | Height: | Size: 827 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 887 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 827 KiB After Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 603 KiB |
|
Before Width: | Height: | Size: 14 MiB After Width: | Height: | Size: 781 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 7.2 MiB After Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 4.9 MiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 5.0 MiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 6.8 MiB After Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 6.8 MiB After Width: | Height: | Size: 687 KiB |
|
Before Width: | Height: | Size: 6.8 MiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 6.3 MiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 4.9 MiB After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 459 KiB After Width: | Height: | Size: 310 KiB |