Merge dev into main (v0.16.1)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 27s

This commit is contained in:
sandrodach committed 2026-10-31 09:52:47 +01:00
commit 5fff5d4361
18 files changed
+83 -37

No files matched your search

+10 -1
View File
@@ -6,6 +6,14 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased] ## [Unreleased]
## [0.16.1] - 2026-10-31
### Changed
- README-Screenshots aufgeräumt: Nur noch das Titelbild ist diagonal hell/dunkel geteilt, Handy und übrige Ansichten zeigen jeweils ein Theme. Neu ist ein Raster mit den Farbfamilien Fjord, Terrakotta, Polarized und Suri (je hell/dunkel diagonal, im Kanban-Board).
### Fixed
- Einstellungen am Handy: Bei der Tageszusammenfassung ragten die Kanal-Schalter (E-Mail, Discord, Webhook, Push) rechts aus dem Bild. Am Handy stehen sie jetzt zu zweit untereinander.
## [0.16.0] - 2026-10-31 ## [0.16.0] - 2026-10-31
### Added ### Added
@@ -374,7 +382,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.0...dev [Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.1...dev
[0.16.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.0...v0.16.1
[0.16.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...v0.16.0 [0.16.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...v0.16.0
[0.15.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.0...v0.15.1 [0.15.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.0...v0.15.1
[0.15.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...v0.15.0 [0.15.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...v0.15.0
+15 -4
View File
@@ -6,7 +6,7 @@
[![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#)
[![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart)
[![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir)
[![Version](https://img.shields.io/badge/Version-v0.15.0-16a34a?style=flat-square)](CHANGELOG.md) [![Version](https://img.shields.io/badge/Version-v0.16.1-16a34a?style=flat-square)](CHANGELOG.md)
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
@@ -14,7 +14,7 @@
<img src="screenshots/Triage_desktop.png" alt="MindSift Triage am Desktop, links hell, rechts dunkel" width="85%" /> <img src="screenshots/Triage_desktop.png" alt="MindSift Triage am Desktop, links hell, rechts dunkel" width="85%" />
</p> </p>
<sub>Alle Screenshots zeigen links das helle und rechts das dunkle Theme.</sub> <sub>Links das helle, rechts das dunkle Theme.</sub>
[Philosophie](#-die-philosophie) • [Funktionen](#-funktionen) • [Schnellstart](#-schnellstart) • [Betrieb](#-betrieb--konfiguration) • [Entwicklung](#-entwicklung) • [Dokumentation](#-dokumentation) [Philosophie](#-die-philosophie) • [Funktionen](#-funktionen) • [Schnellstart](#-schnellstart) • [Betrieb](#-betrieb--konfiguration) • [Entwicklung](#-entwicklung) • [Dokumentation](#-dokumentation)
@@ -43,9 +43,9 @@ Die meisten Notiz- und Aufgaben-Apps verlangen Struktur im falschen Moment: Wenn
* Papierkorb mit Wiederherstellen. * Papierkorb mit Wiederherstellen.
<p align="center"> <p align="center">
<img src="screenshots/Mobile_Triage.png" alt="MindSift Triage am Smartphone" width="280px" /> <img src="screenshots/Mobile_Triage.png" alt="MindSift Triage am Smartphone, helles Theme" width="280px" />
&nbsp;&nbsp; &nbsp;&nbsp;
<img src="screenshots/Mobile_Workspace.png" alt="MindSift Notiz am Smartphone" width="280px" /> <img src="screenshots/Mobile_Workspace.png" alt="MindSift Notiz am Smartphone, dunkles Theme" width="280px" />
</p> </p>
### ✅ Aufgaben & Kanban ### ✅ Aufgaben & Kanban
@@ -115,6 +115,17 @@ Technik dahinter: [Doku/modules/collaboration.md](Doku/modules/collaboration.md)
* **Fünf Farbfamilien** mit je einer hellen und dunklen Variante: Klassik, Fjord, Terrakotta, Polarized, Suri. Hell/Dunkel auf Wunsch „Wie System“, dazu eine eigene Akzentfarbe, Ecken rund oder eckig und Knöpfe mit oder ohne Rahmen (pro Gerät). Für eigene Themes gibt es einen Theme-Manager (eine CSS-Datei). * **Fünf Farbfamilien** mit je einer hellen und dunklen Variante: Klassik, Fjord, Terrakotta, Polarized, Suri. Hell/Dunkel auf Wunsch „Wie System“, dazu eine eigene Akzentfarbe, Ecken rund oder eckig und Knöpfe mit oder ohne Rahmen (pro Gerät). Für eigene Themes gibt es einen Theme-Manager (eine CSS-Datei).
* **Installierbar als App (PWA)** auf Smartphone und Desktop. Sie startet auch ohne Netz mit dem zuletzt geladenen Stand; offline erfasste Gedanken und abgehakte Aufgaben oder Listeneinträge werden nachgereicht, sobald die Verbindung wieder da ist. * **Installierbar als App (PWA)** auf Smartphone und Desktop. Sie startet auch ohne Netz mit dem zuletzt geladenen Stand; offline erfasste Gedanken und abgehakte Aufgaben oder Listeneinträge werden nachgereicht, sobald die Verbindung wieder da ist.
<table>
<tr>
<td align="center"><img src="screenshots/Theme_fjord.png" alt="Farbfamilie Fjord, links hell, rechts dunkel" /><br /><sub><b>Fjord</b></sub></td>
<td align="center"><img src="screenshots/Theme_terracotta.png" alt="Farbfamilie Terrakotta, links hell, rechts dunkel" /><br /><sub><b>Terrakotta</b></sub></td>
</tr>
<tr>
<td align="center"><img src="screenshots/Theme_polarized.png" alt="Farbfamilie Polarized, links hell, rechts dunkel" /><br /><sub><b>Polarized</b></sub></td>
<td align="center"><img src="screenshots/Theme_suri.png" alt="Farbfamilie Suri, links hell, rechts dunkel" /><br /><sub><b>Suri</b></sub></td>
</tr>
</table>
### 🔌 Integrationen ### 🔌 Integrationen
* **Eingehend (API-Key):** `POST /api/v1/webhooks/ingest` legt Gedanken, Aufgaben oder Listeneinträge aus Smarthome, Skripten oder Automationen an. * **Eingehend (API-Key):** `POST /api/v1/webhooks/ingest` legt Gedanken, Aufgaben oder Listeneinträge aus Smarthome, Skripten oder Automationen an.
+47 -22
View File
@@ -1,6 +1,7 @@
// Takes the README screenshots from a fresh server in demo mode (sample data of admin/admin). // Takes the README screenshots from a fresh server in demo mode (sample data of admin/admin).
// Every picture is taken twice in the same state – light and dark theme – and the two halves are // The title picture and the colour-family grid are taken twice in the same state – light and dark
// joined along a slanted diagonal (light left, dark right). // theme – and joined along a slanted diagonal (light left, dark right); the others are plain light
// or dark pictures.
// Usage: e2e/screenshots.sh (builds the image) or MINDSIFT_URL=… SHOTS_DIR=… node screenshots.mjs // Usage: e2e/screenshots.sh (builds the image) or MINDSIFT_URL=… SHOTS_DIR=… node screenshots.mjs
// Theme pair: SHOT_LIGHT / SHOT_DARK (default: light / dark, the "Klassik" family) // Theme pair: SHOT_LIGHT / SHOT_DARK (default: light / dark, the "Klassik" family)
import fs from 'node:fs'; import fs from 'node:fs';
@@ -13,6 +14,13 @@ const DARK = process.env.SHOT_DARK || 'dark';
const DESKTOP = { viewport: { width: 1518, height: 886 }, deviceScaleFactor: 2 }; const DESKTOP = { viewport: { width: 1518, height: 886 }, deviceScaleFactor: 2 };
const MOBILE = { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true }; const MOBILE = { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Colour families for the theme grid in the README (Klassik is the title picture)
const FAMILIES = [
['fjord', 'fjord', 'fjord-night'],
['terracotta', 'terracotta', 'terracotta-night'],
['polarized', 'polarized-light', 'polarized'],
['suri', 'suri-light', 'suri'],
];
async function login(user) { async function login(user) {
const res = await fetch(`${BASE}/api/v1/auth/login`, { const res = await fetch(`${BASE}/api/v1/auth/login`, {
@@ -57,10 +65,11 @@ async function setTheme(page, id, dark) {
} }
let composer; let composer;
// Light picture on the left of the diagonal, dark on the right, a thin line between them // Light picture on the left of the diagonal, dark on the right, a thin line between them;
async function compose(lightPng, darkPng, file) { // scale < 1 shrinks the result (small pictures of the theme grid)
async function compose(lightPng, darkPng, file, scale = 1) {
const dataUrl = await composer.evaluate( const dataUrl = await composer.evaluate(
async ([light, dark]) => { async ([light, dark, scale]) => {
const load = (b64) => const load = (b64) =>
new Promise((resolve) => { new Promise((resolve) => {
const img = new Image(); const img = new Image();
@@ -68,13 +77,14 @@ async function compose(lightPng, darkPng, file) {
img.src = 'data:image/png;base64,' + b64; img.src = 'data:image/png;base64,' + b64;
}); });
const [a, b] = await Promise.all([load(light), load(dark)]); const [a, b] = await Promise.all([load(light), load(dark)]);
const w = a.naturalWidth; const w = Math.round(a.naturalWidth * scale);
const h = a.naturalHeight; const h = Math.round(a.naturalHeight * scale);
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = w; canvas.width = w;
canvas.height = h; canvas.height = h;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.drawImage(b, 0, 0); ctx.imageSmoothingQuality = 'high';
ctx.drawImage(b, 0, 0, w, h);
const top = w * 0.58; const top = w * 0.58;
const bottom = w * 0.42; const bottom = w * 0.42;
ctx.save(); ctx.save();
@@ -85,7 +95,7 @@ async function compose(lightPng, darkPng, file) {
ctx.lineTo(0, h); ctx.lineTo(0, h);
ctx.closePath(); ctx.closePath();
ctx.clip(); ctx.clip();
ctx.drawImage(a, 0, 0); ctx.drawImage(a, 0, 0, w, h);
ctx.restore(); ctx.restore();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = Math.max(2, w / 600); ctx.lineWidth = Math.max(2, w / 600);
@@ -97,19 +107,30 @@ async function compose(lightPng, darkPng, file) {
ctx.stroke(); ctx.stroke();
return canvas.toDataURL('image/png'); return canvas.toDataURL('image/png');
}, },
[lightPng.toString('base64'), darkPng.toString('base64')] [lightPng.toString('base64'), darkPng.toString('base64'), scale]
); );
fs.writeFileSync(`${OUT}/${file}`, Buffer.from(dataUrl.split(',')[1], 'base64')); fs.writeFileSync(`${OUT}/${file}`, Buffer.from(dataUrl.split(',')[1], 'base64'));
} }
async function shot(page, file) { // Diagonal light/dark picture
async function shot(page, file, { light = LIGHT, dark = DARK, scale = 1 } = {}) {
await sleep(800); // animations await sleep(800); // animations
await page.mouse.move(0, 0); await page.mouse.move(0, 0);
await setTheme(page, LIGHT, false); await setTheme(page, light, false);
const light = await page.screenshot(); const lightPng = await page.screenshot();
await setTheme(page, DARK, true); await setTheme(page, dark, true);
const dark = await page.screenshot(); const darkPng = await page.screenshot();
await compose(light, dark, file); await compose(lightPng, darkPng, file, scale);
console.log(`OK ${file}`);
}
// Plain picture in one theme ('light' or 'dark' of the chosen pair)
async function plain(page, file, mode) {
await sleep(800);
await page.mouse.move(0, 0);
if (mode === 'light') await setTheme(page, LIGHT, false);
else await setTheme(page, DARK, true);
await page.screenshot({ path: `${OUT}/${file}` });
console.log(`OK ${file}`); console.log(`OK ${file}`);
} }
@@ -126,7 +147,11 @@ const main = async () => {
await tab(page, 'Aufgaben'); await tab(page, 'Aufgaben');
await page.getByText('Homelab', { exact: true }).first().click().catch(() => {}); await page.getByText('Homelab', { exact: true }).first().click().catch(() => {});
await page.getByRole('button', { name: /Kanban|Board/ }).first().click().catch(() => {}); await page.getByRole('button', { name: /Kanban|Board/ }).first().click().catch(() => {});
await shot(page, 'aufgaben_desktop.png'); await plain(page, 'aufgaben_desktop.png', 'dark');
// Colour families: the board fills the width, so both halves of the diagonal show content
for (const [family, light, dark] of FAMILIES) {
await shot(page, `Theme_${family}.png`, { light, dark, scale: 0.5 });
}
// The demo user edits a bullet of the shared note, so the screenshot shows lock and presence // The demo user edits a bullet of the shared note, so the screenshot shows lock and presence
const other = await open(browser, user, DESKTOP); const other = await open(browser, user, DESKTOP);
@@ -142,7 +167,7 @@ const main = async () => {
await page.waitForFunction(() => document.querySelectorAll('textarea').length > 5, null, { timeout: 10000 }); await page.waitForFunction(() => document.querySelectorAll('textarea').length > 5, null, { timeout: 10000 });
await page.getByText('User', { exact: false }).first().waitFor({ timeout: 15000 }).catch(() => {}); await page.getByText('User', { exact: false }).first().waitFor({ timeout: 15000 }).catch(() => {});
await sleep(2000); await sleep(2000);
await shot(page, 'workspace_desktop.png'); await plain(page, 'workspace_desktop.png', 'light');
// Journal: a few days with entries, the calendar in the sidebar // Journal: a few days with entries, the calendar in the sidebar
const H = { Authorization: `Bearer ${admin}`, 'Content-Type': 'application/json' }; const H = { Authorization: `Bearer ${admin}`, 'Content-Type': 'application/json' };
@@ -171,22 +196,22 @@ const main = async () => {
await page.getByRole('button', { name: 'Heute' }).click().catch(() => {}); await page.getByRole('button', { name: 'Heute' }).click().catch(() => {});
await page.mouse.click(1000, 860); await page.mouse.click(1000, 860);
await sleep(2500); await sleep(2500);
await shot(page, 'journal_desktop.png'); await plain(page, 'journal_desktop.png', 'dark');
await page.getByRole('button', { name: 'Journal', exact: true }).click(); // close it again await page.getByRole('button', { name: 'Journal', exact: true }).click(); // close it again
await tab(page, 'Listen'); await tab(page, 'Listen');
await page.getByText('Wocheneinkauf', { exact: true }).first().click().catch(() => {}); await page.getByText('Wocheneinkauf', { exact: true }).first().click().catch(() => {});
await shot(page, 'Listen_desktop.png'); await plain(page, 'Listen_desktop.png', 'light');
const mobile = await open(browser, admin, MOBILE); const mobile = await open(browser, admin, MOBILE);
await mobile.getByText('Später im Browser weiter nutzen').click({ timeout: 5000 }).catch(() => {}); // install prompt await mobile.getByText('Später im Browser weiter nutzen').click({ timeout: 5000 }).catch(() => {}); // install prompt
await shot(mobile, 'Mobile_Triage.png'); await plain(mobile, 'Mobile_Triage.png', 'light');
await tab(mobile, 'Workspace'); await tab(mobile, 'Workspace');
await sleep(600); await sleep(600);
await mobile.getByText(/^Homelab 2\.0/).first().click(); await mobile.getByText(/^Homelab 2\.0/).first().click();
await sleep(1500); await sleep(1500);
await shot(mobile, 'Mobile_Workspace.png'); await plain(mobile, 'Mobile_Workspace.png', 'dark');
await browser.close(); await browser.close();
}; };
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Builds MindSift, starts it in demo mode with a fresh database and renews the README screenshots # Builds MindSift, starts it in demo mode with a fresh database and renews the README screenshots
# in screenshots/ (light and dark theme joined along a diagonal). Requires podman (or docker via # in screenshots/ (title picture and colour-family grid: light and dark joined along a diagonal).
# ENGINE=docker). Usage: e2e/screenshots.sh, other theme pair: SHOT_LIGHT=fjord SHOT_DARK=fjord-night # Requires podman (or docker via ENGINE=docker). Usage: e2e/screenshots.sh, other theme pair: SHOT_LIGHT=fjord SHOT_DARK=fjord-night
set -euo pipefail set -euo pipefail
ENGINE="${ENGINE:-podman}" ENGINE="${ENGINE:-podman}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.16.0", "version": "0.16.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.16.0", "version": "0.16.1",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "5.3.0", "@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"private": true, "private": true,
"version": "0.16.0", "version": "0.16.1",
"license": "Unlicense", "license": "Unlicense",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -1534,8 +1534,9 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
/> />
</label> </label>
<div className="flex items-center gap-5"> {/* Phones: 2×2 grid, the four switches do not fit in one row */}
<div className="flex items-center gap-2"> <div className="grid grid-cols-2 gap-x-6 gap-y-3 w-full sm:w-auto sm:flex sm:items-center sm:gap-5">
<div className="flex items-center justify-between sm:justify-start gap-2">
<span className="text-xs text-muted">E-Mail</span> <span className="text-xs text-muted">E-Mail</span>
<ToggleSwitch <ToggleSwitch
checked={notificationSettings.dailySummary.email} checked={notificationSettings.dailySummary.email}
@@ -1548,7 +1549,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
title="Tageszusammenfassung – E-Mail" title="Tageszusammenfassung – E-Mail"
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center justify-between sm:justify-start gap-2">
<span className="text-xs text-muted">Discord</span> <span className="text-xs text-muted">Discord</span>
<ToggleSwitch <ToggleSwitch
checked={notificationSettings.dailySummary.discord} checked={notificationSettings.dailySummary.discord}
@@ -1566,7 +1567,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
} }
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center justify-between sm:justify-start gap-2">
<span className="text-xs text-muted">Webhook</span> <span className="text-xs text-muted">Webhook</span>
<ToggleSwitch <ToggleSwitch
checked={notificationSettings.dailySummary.webhook} checked={notificationSettings.dailySummary.webhook}
@@ -1584,7 +1585,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
} }
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center justify-between sm:justify-start gap-2">
<span className="text-xs text-muted">Push</span> <span className="text-xs text-muted">Push</span>
<ToggleSwitch <ToggleSwitch
checked={notificationSettings.dailySummary.push} checked={notificationSettings.dailySummary.push}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 294 KiB