feat: copy song lists with jackets and simplify song rows
Publish GitHub Pages output / publish (push) Successful in 38s

This commit is contained in:
2026-10-07 21:58:31 +09:00
parent 89e9976ce9
commit a9e140fa85
220 changed files with 2070 additions and 50 deletions
+5 -1
View File
@@ -1,9 +1,13 @@
---
updatedAt: 2026-10-07T08:23:52.000Z
updatedAt: 2026-10-07T12:57:51.000Z
---
## 2026년 10월 7일
### 악곡 목록에 재킷 함께 복사
`IMPROVED` 악곡 목록의 ‘목록 복사’에 곡 재킷을 추가했습니다. 번호·재킷·제목 순서로 복사하며, 재킷은 작은 이미지로, 번호와 제목은 편집 가능한 텍스트로 유지합니다. 난이도·레벨은 검색 결과, 선택 목록, 복사 내용에서 제외하고 난이도 선택 항목을 제거했습니다. 일반 텍스트 입력란에는 번호·제목만 붙여넣고, 자동 복사를 사용할 수 없으면 재킷과 텍스트가 있는 복사용 목록을 직접 선택해 복사할 수 있습니다.
### 카드·의상 효과 메뉴 분리
`NEW` 카드 목록 상단에 ‘카드 / 의상 효과’ 서브 메뉴를 추가했습니다. ★4·★5 리더 의상을 발동 조건의 속성·기수, 강화 효과, 획득 카드 등급·이격, 멤버 기수로 필터링할 수 있습니다. 스탯 증가율과 스코어 서포트 수치를 각각 정렬하며, 두 효과가 함께 있는 의상도 각각 표시합니다. 강화 효과의 스탯은 마크와 이름을 함께 표시합니다. 여러 의상의 상세를 동시에 펼쳐 비교할 수 있고 필터·정렬 설정은 다음 방문에도 유지합니다. 카드 상세의 의상 효과에서 해당 의상으로 바로 이동해 상세를 펼칠 수 있습니다.
+6
View File
@@ -38,6 +38,12 @@
| 과거 이벤트 기간·배율 | `src/lib/data/events.ts`의 기존 수동 기록. 당시 외부 출처 주석을 유지. 현재 DB에서 이벤트별 설정을 찾지 못함. 기존 기록의 기간·배율은 그대로 보존 |
| 카드 별칭·표시명 | `data/card-overrides.csv`의 사용자 편집 설정 |
### 악곡 재킷 이미지
`src/lib/data/song-jackets.json`은 현재 앱 악곡 카탈로그의 곡 ID를 해시가 고정된 Android `Music.json`의 `jacketAssetId`에 연결합니다. 이미지는 별도 공식 게임 CDN의 `img_music_jacket_<jacketAssetId>` 번들에서 추출했으며, DB에서 추출한 이미지로 표시하지 않습니다.
2026-10-07 Octo revision 97에서 앱에 포함된 214곡의 번들 MD5를 확인하고 Texture2D를 최대 128px WebP로 변환했습니다. `static/images/song-jackets/`에 저장하고, 메타데이터에 번들 이름·MD5와 생성 이미지 SHA-256을 기록합니다. 재킷 추가·변경 시 현재 앱 카탈로그와 같은 DB 스냅샷의 매핑 및 공식 CDN 번들 해시를 다시 확인해야 합니다. 미래 공개 악곡과 카탈로그 밖의 자산은 포함하지 않습니다.
이번 전환에서 기존 200곡/800채보를 로컬 게임 파일로 다시 생성하고, 공식 게임 CDN의 Octo revision 93에서 신규·누락 9곡의 SUS 36개를 확보했습니다. 현재 공개된 209곡/836채보를 생성했습니다. 아직 공개되지 않은 2곡은 DB 스냅샷에만 보관합니다.
공식 게임 자산과 서버의 이벤트 설정까지 DB 하나에 들어 있는 것은 아닙니다. HolodoriDB 원격 저장소나 다른 계산기 사이트 접속은 기본 수집 과정에서 필요하지 않습니다.
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync, existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { formatSongListClipboard } from './song-list-copy.ts';
test('복사 목록은 중복·선택 순서를 보존하고 번호·곡명만 텍스트로 제공한다', () => {
const songs = [{ songId: 'm0001', title: ' A\nB ', difficulty: 'expert', difficultyLevel: 30 }];
const result = formatSongListClipboard([...songs, ...songs], { m0001: { path: '/images/song-jackets/m0001.webp' } }, 'https://doricalc-holo.live');
assert.equal(result.text, '1. A B\n2. A B');
assert.equal(result.html.match(/<img /g)?.length, 2);
assert.ok(result.html.includes('src="https://doricalc-holo.live/images/song-jackets/m0001.webp"'));
assert.ok(result.html.includes('<td style="border:0;padding:4px 8px;vertical-align:middle;">A B</td>'));
assert.ok(result.html.indexOf('>1.</td>') < result.html.indexOf('<img '));
assert.ok(result.html.indexOf('<img ') < result.html.indexOf('>A B</td>'));
assert.ok(!result.html.includes('expert'));
assert.equal(result.missingJackets, 0);
});
test('곡명은 HTML을 이스케이프하고 재킷은 검증된 로컬 이미지 경로만 사용한다', () => {
const result = formatSongListClipboard([{ songId: 'm0001', title: '<script>"A" & B</script>' }], { m0001: { path: 'https://example.com/evil.webp' } }, 'https://doricalc-holo.live');
assert.ok(result.html.includes('&lt;script&gt;&quot;A&quot; &amp; B&lt;/script&gt;'));
assert.ok(!result.html.includes('<img'));
assert.equal(result.missingJackets, 1);
});
test('빈 목록에는 복사할 HTML·텍스트가 없다', () => {
assert.deepEqual(formatSongListClipboard([], {}, 'https://doricalc-holo.live'), { text: '', html: '', missingJackets: 0 });
});
test('현재 악곡 카탈로그 전체의 재킷 파일과 출처 해시를 검증한다', () => {
const metadata = JSON.parse(readFileSync('src/lib/data/song-jackets.json', 'utf8'));
const charts = JSON.parse(readFileSync('src/lib/data/exact-chart-catalog.json', 'utf8')).charts;
for (const songId of new Set<string>(charts.map((chart: { songId: string }) => chart.songId))) {
const jacket = metadata.jackets[songId];
assert.ok(jacket, `Missing jacket: ${songId}`);
assert.match(jacket.assetName, /^img_music_jacket_m\d{4}$/);
assert.match(jacket.path, /^\/images\/song-jackets\/m\d{4}\.webp$/);
assert.ok(existsSync(`static${jacket.path}`));
assert.equal(createHash('sha256').update(readFileSync(`static${jacket.path}`)).digest('hex'), jacket.sha256);
}
});
+30
View File
@@ -0,0 +1,30 @@
import type { SongChartSummary } from '../data/song-catalog.ts';
type Song = Pick<SongChartSummary, 'songId' | 'title'>;
type Jackets = Readonly<Record<string, { path: string }>>;
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, character => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[character]!);
}
/** Rich editors receive jacket images and editable text; plain editors receive the same numbered titles. */
export function formatSongListClipboard(songs: readonly Song[], jackets: Jackets, origin: string) {
let missingJackets = 0;
const titles = songs.map(song => song.title.replace(/\s+/g, ' ').trim());
const rows = songs.map((song, index) => {
const path = jackets[song.songId]?.path;
// Only allow our generated static assets, never arbitrary markup or external image URLs.
const src = path && /^\/images\/song-jackets\/m\d{4}\.webp$/.test(path)
? new URL(path, origin).href : undefined;
if (!src) missingJackets++;
const image = src ? `<img src="${escapeHtml(src)}" width="48" height="48" alt="" style="display:block;width:48px;height:48px;">` : '';
return `<tr><td style="border:0;padding:4px 8px;vertical-align:middle;white-space:nowrap;">${index + 1}.</td><td style="border:0;padding:4px;vertical-align:middle;">${image}</td><td style="border:0;padding:4px 8px;vertical-align:middle;">${escapeHtml(titles[index])}</td></tr>`;
});
return {
text: titles.map((title, index) => `${index + 1}. ${title}`).join('\n'),
html: songs.length ? `<table border="0" cellpadding="0" cellspacing="0" style="border-collapse:collapse;width:auto;table-layout:auto;"><tbody>${rows.join('')}</tbody></table>` : '',
missingJackets
};
}
+46 -49
View File
@@ -3,35 +3,34 @@
import { page } from '$app/stores';
import ToolsNav from '$lib/components/ToolsNav.svelte';
import exactChartCatalog from '$lib/data/exact-chart-catalog.json';
import { difficulties, difficultyLabel } from '$lib/data/difficulty.ts';
import songJackets from '$lib/data/song-jackets.json';
import { createInitialReleaseClock, synchronizeReleaseClock } from '$lib/data/release-clock.ts';
import { songOptions } from '$lib/data/song-catalog.ts';
import { releasedSongCharts } from '$lib/data/song-release.ts';
import { formatRandomSongResults } from '$lib/engine/random-song.ts';
import { formatSongListClipboard } from '$lib/engine/song-list-copy.ts';
import { SONG_LIST_STORAGE_KEY, parseSavedSongList, serializeSongList } from '$lib/storage/song-list.ts';
type Chart = (typeof exactChartCatalog.charts)[number];
let releaseClock = createInitialReleaseClock($page.data?.serverNow);
let releaseNow = releaseClock.now();
let query = '';
let difficulty: string = 'expert';
let selections: { id: number; chart: Chart }[] = [];
let nextId = 0;
let announcement = '';
let copying = false;
let copyStatus = '';
let showCopyText = false;
let copyTextArea: HTMLTextAreaElement | undefined;
let showCopyPreview = false;
let copyPreview: HTMLDivElement | undefined;
let copyOrigin = 'https://doricalc.cc';
let storageNotice = '';
$: charts = releasedSongCharts(exactChartCatalog.charts, releaseNow);
$: difficultyCharts = charts.filter((chart) => chart.difficulty === difficulty);
$: options = songOptions(difficultyCharts, difficulty, query);
$: resultsText = formatRandomSongResults(selections.map(({ chart }) => chart));
$: options = songOptions(charts, '', query);
$: resultsClipboard = formatSongListClipboard(selections.map(({ chart }) => chart), songJackets.jackets, copyOrigin);
function resetCopyStatus() {
copyStatus = '';
showCopyText = false;
showCopyPreview = false;
}
function saveSongs() {
@@ -48,12 +47,12 @@
}
function addSong(songId: string) {
const chart = difficultyCharts.find((chart) => chart.songId === songId);
const chart = charts.find((chart) => chart.songId === songId);
if (!chart) return;
selections = [...selections, { id: nextId++, chart }];
saveSongs();
resetCopyStatus();
announcement = `${selections.length}번째에 ${chart.title}, ${difficultyLabel(chart.difficulty)}를 추가했습니다.`;
announcement = `${selections.length}번째에 ${chart.title}를 추가했습니다.`;
}
function removeSong(id: number) {
@@ -74,26 +73,39 @@
async function copySongs() {
if (!selections.length || copying) return;
const snapshot = selections;
const text = resultsText;
const content = resultsClipboard;
copying = true;
resetCopyStatus();
try {
if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable');
await navigator.clipboard.writeText(text);
if (selections === snapshot) copyStatus = `${snapshot.length}개 악곡을 선택 순서대로 복사했습니다.`;
if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined') throw new Error('Rich clipboard unavailable');
await navigator.clipboard.write([new ClipboardItem({
'text/html': new Blob([content.html], { type: 'text/html' }),
'text/plain': new Blob([content.text], { type: 'text/plain' })
})]);
if (selections === snapshot) {
copyStatus = `${snapshot.length}개 악곡의 번호·재킷·제목을 선택 순서대로 복사했습니다.`;
if (content.missingJackets) copyStatus += ` 재킷 정보가 없는 ${content.missingJackets}개는 번호·곡명만 포함했습니다.`;
}
} catch {
if (selections !== snapshot) return;
copyStatus = '자동 복사를 사용할 수 없습니다. 아래 텍스트를 선택해 복사해 주세요.';
showCopyText = true;
copyStatus = '자동 복사를 사용할 수 없습니다. 아래 선택된 목록을 직접 복사해 주세요.';
showCopyPreview = true;
await tick();
copyTextArea?.focus();
copyTextArea?.select();
if (copyPreview) {
copyPreview.focus();
const range = document.createRange();
range.selectNodeContents(copyPreview);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
} finally {
copying = false;
}
}
onMount(() => {
copyOrigin = window.location.origin;
try {
const text = localStorage.getItem(SONG_LIST_STORAGE_KEY);
if (text) {
@@ -117,7 +129,7 @@
<svelte:head>
<title>악곡 목록 · 도구 · 갤산기</title>
<meta name="description" content="악곡과 난이도를 고른 순서대로 목록을 만들고 복사하세요." />
<meta name="description" content="악곡을 고른 순서대로 목록을 만들고 복사하세요." />
</svelte:head>
<main>
@@ -136,26 +148,20 @@
<label>악곡 검색
<input type="search" bind:value={query} placeholder="곡명 검색" autocomplete="off">
</label>
<label>난이도
<select bind:value={difficulty}>
{#each difficulties as item}<option value={item}>{difficultyLabel(item)}</option>{/each}
</select>
</label>
</div>
{#if options.length}
<ul class="song-options" aria-label="추가할 악곡">
{#each options as song (song.id)}
<li>
<button class="song-option" type="button" on:click={() => addSong(song.id)} aria-label={`${song.title}, ${difficultyLabel(difficulty)} 목록에 추가`}>
<button class="song-option" type="button" on:click={() => addSong(song.id)} aria-label={`${song.title} 목록에 추가`}>
<strong class="song-title">{song.title}</strong>
<span class="level">{song.difficultyLevel == null ? '레벨 정보 없음' : `Lv. ${song.difficultyLevel}`}</span>
<span class="add-label" aria-hidden="true">+ 추가</span>
</button>
</li>
{/each}
</ul>
{:else}
<p class="placeholder" role="status">검색 결과가 없습니다. 검색어나 난이도를 변경해 주세요.</p>
<p class="placeholder" role="status">검색 결과가 없습니다. 검색어를 변경해 주세요.</p>
{/if}
</section>
@@ -170,10 +176,10 @@
</div>
{#if storageNotice}<p class="storage-notice" role="status">{storageNotice}</p>{/if}
<p class="copy-status" role="status">{copyStatus}</p>
{#if showCopyText}
<label class="copy-text">복사용 텍스트
<textarea bind:this={copyTextArea} value={resultsText} readonly rows={Math.min(selections.length + 1, 8)} on:focus={(event) => event.currentTarget.select()}></textarea>
</label>
{#if showCopyPreview}
<div class="copy-preview" bind:this={copyPreview} tabindex="-1" aria-label="복사용 악곡 목록">
{@html resultsClipboard.html}
</div>
{/if}
{#if selections.length}
<ol class="results-list" aria-label="악곡 선택 순서">
@@ -181,8 +187,6 @@
<li>
<span class="selection-number" aria-hidden="true">{index + 1}</span>
<strong class="song-title selected-title">{entry.chart.title}</strong>
<span class="selected-difficulty">{difficultyLabel(entry.chart.difficulty)}</span>
<span class="selected-level">{entry.chart.difficultyLevel == null ? '레벨 정보 없음' : `Lv. ${entry.chart.difficultyLevel}`}</span>
<button class="secondary-button remove-button" type="button" on:click={() => removeSong(entry.id)} aria-label={`${index + 1}번째 ${entry.chart.title} 삭제`}>삭제</button>
</li>
{/each}
@@ -202,21 +206,19 @@
h2 span { margin-left: 6px; color: #a4adba; font-size: 14px; font-weight: 500; }
.panel, .result { margin-top: 26px; padding: 26px; border: 1px solid #343c49; border-radius: 16px; background: #151a22; }
.panel { padding: 20px; }
.filters { display: grid; grid-template-columns: minmax(0, 1fr) 140px; gap: 14px; }
.filters { display: grid; grid-template-columns: minmax(0, 1fr); }
label { display: grid; gap: 8px; min-width: 0; color: #a4adba; font-size: 14px; }
input, select, textarea { width: 100%; min-width: 0; padding: 12px; border: 1px solid #414b5b; border-radius: 9px; background: #101319; color: #e8ebf1; font: inherit; }
input, select { height: 44px; }
input { width: 100%; min-width: 0; height: 44px; padding: 12px; border: 1px solid #414b5b; border-radius: 9px; background: #101319; color: #e8ebf1; font: inherit; }
.placeholder { color: #a4adba; line-height: 1.7; }
.song-options { max-height: 200px; overflow-y: auto; overscroll-behavior: contain; margin: 12px 0 0; padding: 3px; list-style: none; }
.song-options li + li { margin-top: 4px; }
button { min-height: 44px; border: 1px solid #343c49; border-radius: 9px; background: #101319; color: #e8ebf1; font: inherit; cursor: pointer; }
button:hover:not(:disabled) { border-color: #a76b87; background: #302331; }
button:disabled { color: #87909e; cursor: not-allowed; }
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 3px solid #ed8bb5; outline-offset: 2px; }
.song-option { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 10px; width: 100%; min-height: 36px; padding: 6px 10px; border-radius: 7px; text-align: left; }
button:focus-visible, input:focus-visible, .copy-preview:focus-visible { outline: 3px solid #ed8bb5; outline-offset: 2px; }
.song-option { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; width: 100%; min-height: 36px; padding: 6px 10px; border-radius: 7px; text-align: left; }
.song-title { min-width: 0; overflow-wrap: anywhere; line-height: 1.6; }
.song-option .song-title { font-size: 14px; line-height: 1.35; }
.level { color: #a4adba; font-size: 13px; }
.add-label { color: #f3b4d0; font-size: 13px; font-weight: 800; white-space: nowrap; }
.result { padding: 20px; }
.results-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
@@ -227,26 +229,21 @@
.copy-status { margin: 12px 0 0; color: #f3b4d0; font-size: 13px; line-height: 1.6; }
.copy-status:empty { margin: 0; }
.storage-notice { color: #f3b4d0; font-size: 13px; line-height: 1.7; }
.copy-text { margin-top: 12px; }
textarea { min-height: 88px; resize: vertical; line-height: 1.7; }
.copy-preview { margin-top: 12px; max-height: 320px; overflow: auto; padding: 12px; border-radius: 9px; background: #fff; color: #222; font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; }
.results-list { margin: 12px 0 0; padding: 0; list-style: none; }
.results-list li { display: grid; grid-template-columns: 24px minmax(0, 1fr) 52px 46px auto; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid #2c3441; }
.results-list li { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid #2c3441; }
.results-list li:last-child { border-bottom: 0; }
.selection-number { color: #a4adba; font-size: 13px; font-variant-numeric: tabular-nums; }
.selected-title { font-size: 14px; line-height: 1.4; }
.selected-difficulty, .selected-level { color: #a4adba; font-size: 12px; }
.selected-level { font-variant-numeric: tabular-nums; }
.remove-button { min-height: 32px; padding: 4px 8px; font-size: 12px; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 520px) {
.panel, .result { padding: 20px; }
.result { padding: 16px; }
.filters { grid-template-columns: minmax(0, 1fr) 110px; gap: 10px; }
.song-option { gap: 8px; padding-inline: 8px; }
.song-option .song-title { font-size: 13px; }
.level, .add-label { font-size: 11px; }
.results-list li { grid-template-columns: 18px minmax(0, 1fr) 42px 36px auto; gap: 6px; }
.add-label { font-size: 11px; }
.results-list li { grid-template-columns: 18px minmax(0, 1fr) auto; gap: 6px; }
.selected-title { font-size: 13px; }
.selected-difficulty, .selected-level { font-size: 11px; }
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Some files were not shown because too many files have changed in this diff Show More