codebase restructure and organization.

This commit is contained in:
2025-11-30 23:16:57 -06:00
parent 80cf812e54
commit 84f887df90
121 changed files with 1952 additions and 17368 deletions

View File

@@ -3,24 +3,35 @@ README.md
LICENSE LICENSE
donate.md donate.md
screenshots/ screenshots/
docs/
# Development files # Development files
.github/ .github/
electron/ electron/
scripts/
Makefile
# Build artifacts and cache # Build artifacts and cache
build/
dist/
public/ public/
node_modules/ node_modules/
__pycache__/ __pycache__/
*.pyc *.py[cod]
*.pyo *$py.class
*.pyd *.so
.Python .Python
*.egg-info/
*.egg
python-dist/
# Virtual environments
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
.venv/
# IDE and editor files # IDE and editor files
.vscode/ .vscode/
@@ -47,9 +58,19 @@ Dockerfile*
docker-compose*.yml docker-compose*.yml
.dockerignore .dockerignore
# Local storage and runtime data
storage/
testing/
telemetry_test_lxmf/
# Logs # Logs
*.log *.log
# Temporary files # Temporary files
*.tmp *.tmp
*.temp *.temp
# Environment variables
.env
.env.local
.env.*.local

View File

@@ -47,11 +47,14 @@ jobs:
with: with:
python-version: "3.12" python-version: "3.12"
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
- name: Sync versions
run: python scripts/sync_version.py
- name: Install Python Deps - name: Install Python Deps
run: | run: python -m poetry install
python -m venv venv
venv\Scripts\pip install --upgrade pip
venv\Scripts\pip install -r requirements.txt
- name: Install NodeJS Deps - name: Install NodeJS Deps
run: npm install run: npm install
@@ -89,11 +92,14 @@ jobs:
with: with:
python-version: "3.11" python-version: "3.11"
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
- name: Sync versions
run: python scripts/sync_version.py
- name: Install Python Deps - name: Install Python Deps
run: | run: python -m poetry install
python3 -m venv venv
venv/bin/pip install --upgrade pip
venv/bin/pip install -r requirements.txt
- name: Install NodeJS Deps - name: Install NodeJS Deps
run: npm install run: npm install
@@ -134,11 +140,21 @@ jobs:
- name: Install patchelf - name: Install patchelf
run: sudo apt-get update && sudo apt-get install -y patchelf run: sudo apt-get update && sudo apt-get install -y patchelf
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
- name: Sync versions
run: python scripts/sync_version.py
- name: Install Python Deps - name: Install Python Deps
run: python -m poetry install
- name: Build Python wheel
run: | run: |
python3 -m venv venv python -m poetry build -f wheel
venv/bin/pip install --upgrade pip mkdir -p python-dist
venv/bin/pip install -r requirements.txt mv dist/*.whl python-dist/
rm -rf dist
- name: Install NodeJS Deps - name: Install NodeJS Deps
run: npm install run: npm install
@@ -155,7 +171,7 @@ jobs:
replacesArtifacts: true replacesArtifacts: true
omitDraftDuringUpdate: true omitDraftDuringUpdate: true
omitNameDuringUpdate: true omitNameDuringUpdate: true
artifacts: "dist/*-linux.AppImage,dist/*-linux.deb" artifacts: "dist/*-linux.AppImage,dist/*-linux.deb,python-dist/*.whl"
build_docker: build_docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest

53
.gitignore vendored
View File

@@ -1,13 +1,56 @@
# IDE and editor files
.idea .idea
node_modules .vscode/
*.swp
*.swo
*~
# build files # Dependencies
node_modules/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
*.egg
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
.venv/
# Build files
/build/ /build/
/dist/ /dist/
/public/ /meshchatx/public/
/electron/build/exe/ /electron/build/exe/
python-dist/
# local storage # Local storage and runtime data
storage/ storage/
testing/
telemetry_test_lxmf/
*.pyc # Logs
*.log
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Environment variables
.env
.env.local
.env.*.local

View File

@@ -1,26 +1,29 @@
.PHONY: install run clean build build-appimage build-exe dist .PHONY: install run develop clean build build-appimage build-exe dist sync-version wheel node_modules python
VENV = venv PYTHON ?= python
PYTHON = $(VENV)/bin/python POETRY = $(PYTHON) -m poetry
PIP = $(VENV)/bin/pip
NPM = npm NPM = npm
install: $(VENV) node_modules install: sync-version node_modules python
$(VENV):
python3 -m venv $(VENV)
$(PIP) install --upgrade pip
$(PIP) install -r requirements.txt
node_modules: node_modules:
$(NPM) install $(NPM) install
python:
$(POETRY) install
run: install run: install
$(PYTHON) meshchat.py $(POETRY) run meshchat
develop: run
build: install build: install
$(NPM) run build $(NPM) run build
wheel: install
$(POETRY) build -f wheel
$(PYTHON) scripts/move_wheels.py
build-appimage: build build-appimage: build
$(NPM) run electron-postinstall $(NPM) run electron-postinstall
$(NPM) run dist -- --linux AppImage $(NPM) run dist -- --linux AppImage
@@ -32,10 +35,10 @@ build-exe: build
dist: build-appimage dist: build-appimage
clean: clean:
rm -rf $(VENV)
rm -rf node_modules rm -rf node_modules
rm -rf build rm -rf build
rm -rf dist rm -rf dist
rm -rf python-dist
sync-version:
$(PYTHON) scripts/sync_version.py

View File

@@ -14,12 +14,12 @@ A heavily customized fork of [Reticulum MeshChat](https://github.com/liamcottle/
- [ ] Multi-language support - [ ] Multi-language support
- [ ] Offline Reticulum documentation tool - [ ] Offline Reticulum documentation tool
- [ ] More tools (translate, LoRa calculator, LXMFy bots, etc) - [ ] More tools (translate, LoRa calculator, LXMFy bots, etc)
- [ ] Codebase reorginization and cleanup. - [x] Codebase reorganization and cleanup.
- [ ] Tests and proper CI/CD pipeline. - [ ] Tests and proper CI/CD pipeline.
- [ ] RNS hot reload - [ ] RNS hot reload
- [ ] Backup/Import identities, messages and interfaces. - [ ] Backup/Import identities, messages and interfaces.
- [ ] Full LXST support. - [ ] Full LXST support.
- [ ] Move to Poetry and pyproject.toml for Python packaging. - [x] Move to Poetry and pyproject.toml for Python packaging.
- [x] More stats on about page. - [x] More stats on about page.
- [x] Actions are pinned to full-length SHA hashes. - [x] Actions are pinned to full-length SHA hashes.
- [x] Docker images are smaller and use SHA256 hashes for the images. - [x] Docker images are smaller and use SHA256 hashes for the images.
@@ -35,23 +35,30 @@ Check [releases](https://github.com/Sudo-Ivan/reticulum-meshchatX/releases) for
## Building ## Building
```bash ```bash
make install make install # installs Python deps via Poetry and Node deps via npm
make build make build
``` ```
You can run `make run` or `make develop` (a thin alias) to start the backend + frontend loop locally through `poetry run meshchat`.
### Python packaging
The Python build is driven entirely by Poetry now. Run `python scripts/sync_version.py` or `make sync-version` before packaging so `pyproject.toml` and `src/version.py` match `package.json`. After that:
```bash
python -m poetry install
make wheel # produces a wheel in python-dist/ that bundles the public assets
```
The wheel includes the frontend `public/` assets, `logo/`, and the CLI entry point, and `python-dist/` keeps the artifact separate from the Electron `dist/` output.
### Building in Docker ### Building in Docker
```bash ```bash
make docker-build make docker-build
``` ```
The build will be in the `dist` directory. The Electron build artifacts will still live under `dist/` for releases.
## Development
```bash
make develop
```
## Python packaging ## Python packaging

View File

@@ -133,6 +133,14 @@ app.whenReady().then(async () => {
webPreferences: { webPreferences: {
// used to inject logging over ipc // used to inject logging over ipc
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
// Security: disable node integration in renderer
nodeIntegration: false,
// Security: enable context isolation (default in Electron 12+)
contextIsolation: true,
// Security: enable sandbox for additional protection
sandbox: true,
// Security: disable remote module (deprecated but explicit)
enableRemoteModule: false,
}, },
}); });

3
meshchatx/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""Reticulum MeshChatX - A mesh network communications app."""
__version__ = "2.41.0"

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone from datetime import UTC, datetime
from peewee import * # noqa: F403 from peewee import * # noqa: F403
from playhouse.migrate import SqliteMigrator from playhouse.migrate import SqliteMigrator
@@ -68,8 +68,8 @@ class Config(BaseModel):
id = BigAutoField() # noqa: F405 id = BigAutoField() # noqa: F405
key = CharField(unique=True) # noqa: F405 key = CharField(unique=True) # noqa: F405
value = TextField() # noqa: F405 value = TextField() # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:
@@ -95,8 +95,8 @@ class Announce(BaseModel):
snr = FloatField(null=True) # noqa: F405 snr = FloatField(null=True) # noqa: F405
quality = FloatField(null=True) # noqa: F405 quality = FloatField(null=True) # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:
@@ -108,8 +108,8 @@ class CustomDestinationDisplayName(BaseModel):
destination_hash = CharField(unique=True) # noqa: F405 # unique destination hash destination_hash = CharField(unique=True) # noqa: F405 # unique destination hash
display_name = CharField() # noqa: F405 # custom display name for the destination hash display_name = CharField() # noqa: F405 # custom display name for the destination hash
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:
@@ -122,8 +122,8 @@ class FavouriteDestination(BaseModel):
display_name = CharField() # noqa: F405 # custom display name for the destination hash display_name = CharField() # noqa: F405 # custom display name for the destination hash
aspect = CharField() # noqa: F405 # e.g: nomadnetwork.node aspect = CharField() # noqa: F405 # e.g: nomadnetwork.node
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:
@@ -159,8 +159,8 @@ class LxmfMessage(BaseModel):
snr = FloatField(null=True) # noqa: F405 snr = FloatField(null=True) # noqa: F405
quality = FloatField(null=True) # noqa: F405 quality = FloatField(null=True) # noqa: F405
is_spam = BooleanField(default=False) # noqa: F405 # if true, message is marked as spam is_spam = BooleanField(default=False) # noqa: F405 # if true, message is marked as spam
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:
@@ -172,8 +172,8 @@ class LxmfConversationReadState(BaseModel):
destination_hash = CharField(unique=True) # noqa: F405 # unique destination hash destination_hash = CharField(unique=True) # noqa: F405 # unique destination hash
last_read_at = DateTimeField() # noqa: F405 last_read_at = DateTimeField() # noqa: F405
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:
@@ -189,8 +189,8 @@ class LxmfUserIcon(BaseModel):
CharField() # noqa: F405 CharField() # noqa: F405
) # hex colour to use for background (background colour) ) # hex colour to use for background (background colour)
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:
@@ -203,8 +203,8 @@ class BlockedDestination(BaseModel):
unique=True, unique=True,
index=True, index=True,
) # unique destination hash that is blocked ) # unique destination hash that is blocked
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:
@@ -217,8 +217,8 @@ class SpamKeyword(BaseModel):
unique=True, unique=True,
index=True, index=True,
) # keyword to match against message content ) # keyword to match against message content
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 created_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) # noqa: F405 updated_at = DateTimeField(default=lambda: datetime.now(UTC)) # noqa: F405
# define table name # define table name
class Meta: class Meta:

View File

@@ -13,7 +13,7 @@ import threading
import time import time
import webbrowser import webbrowser
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, timezone from datetime import UTC, datetime
import LXMF import LXMF
import psutil import psutil
@@ -24,20 +24,21 @@ from LXMF import LXMRouter
from peewee import SqliteDatabase from peewee import SqliteDatabase
from serial.tools import list_ports from serial.tools import list_ports
import database from meshchatx import database
from src.backend.announce_handler import AnnounceHandler from meshchatx.src.backend.announce_handler import AnnounceHandler
from src.backend.async_utils import AsyncUtils from meshchatx.src.backend.async_utils import AsyncUtils
from src.backend.audio_call_manager import AudioCall, AudioCallManager from meshchatx.src.backend.audio_call_manager import AudioCall, AudioCallManager
from src.backend.colour_utils import ColourUtils from meshchatx.src.backend.colour_utils import ColourUtils
from src.backend.interface_config_parser import InterfaceConfigParser from meshchatx.src.backend.interface_config_parser import InterfaceConfigParser
from src.backend.interface_editor import InterfaceEditor from meshchatx.src.backend.interface_editor import InterfaceEditor
from src.backend.lxmf_message_fields import ( from meshchatx.src.backend.lxmf_message_fields import (
LxmfAudioField, LxmfAudioField,
LxmfFileAttachment, LxmfFileAttachment,
LxmfFileAttachmentsField, LxmfFileAttachmentsField,
LxmfImageField, LxmfImageField,
) )
from src.backend.sideband_commands import SidebandCommands from meshchatx.src.backend.sideband_commands import SidebandCommands
from meshchatx.src.version import __version__ as app_version
# NOTE: this is required to be able to pack our app with cxfreeze as an exe, otherwise it can't access bundled assets # NOTE: this is required to be able to pack our app with cxfreeze as an exe, otherwise it can't access bundled assets
@@ -46,9 +47,22 @@ from src.backend.sideband_commands import SidebandCommands
def get_file_path(filename): def get_file_path(filename):
if getattr(sys, "frozen", False): if getattr(sys, "frozen", False):
datadir = os.path.dirname(sys.executable) datadir = os.path.dirname(sys.executable)
else: return os.path.join(datadir, filename)
datadir = os.path.dirname(__file__)
return os.path.join(datadir, filename) # Running from source or an installed wheel: assets live inside the meshchatx package
package_dir = os.path.dirname(__file__)
test_path = os.path.join(package_dir, filename)
if os.path.exists(test_path):
return test_path
# Fall back to repo root when running directly from the source tree
repo_root = os.path.dirname(package_dir)
repo_path = os.path.join(repo_root, filename)
if os.path.exists(repo_path):
return repo_path
# Return the package path even if it does not exist so callers raise a clear error
return test_path
class ReticulumMeshChat: class ReticulumMeshChat:
@@ -225,12 +239,10 @@ class ReticulumMeshChat:
thread.daemon = True thread.daemon = True
thread.start() thread.start()
# gets app version from package.json # gets app version from the synchronized Python version helper
@staticmethod @staticmethod
def get_app_version() -> str: def get_app_version() -> str:
with open(get_file_path("package.json")) as f: return app_version
package_json = json.load(f)
return package_json["version"]
# automatically announces based on user config # automatically announces based on user config
async def announce_loop(self): async def announce_loop(self):
@@ -3011,13 +3023,38 @@ class ReticulumMeshChat:
) )
if message: if message:
message.is_spam = is_spam message.is_spam = is_spam
message.updated_at = datetime.now(timezone.utc) message.updated_at = datetime.now(UTC)
message.save() message.save()
return web.json_response({"message": "ok"}) return web.json_response({"message": "ok"})
return web.json_response({"error": "Message not found"}, status=404) return web.json_response({"error": "Message not found"}, status=404)
except Exception as e: except Exception as e:
return web.json_response({"error": str(e)}, status=500) return web.json_response({"error": str(e)}, status=500)
# security headers middleware
@web.middleware
async def security_middleware(request, handler):
response = await handler(request)
# Add security headers to all responses
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
# CSP: allow localhost for development and Electron, websockets, and blob URLs
csp = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: blob:; "
"font-src 'self' data:; "
"connect-src 'self' ws://localhost:* wss://localhost:* blob:; "
"media-src 'self' blob:; "
"worker-src 'self' blob:; "
"object-src 'none'; "
"base-uri 'self';"
)
response.headers["Content-Security-Policy"] = csp
return response
# called when web app has started # called when web app has started
async def on_startup(app): async def on_startup(app):
# remember main event loop # remember main event loop
@@ -3033,6 +3070,7 @@ class ReticulumMeshChat:
# create and run web app # create and run web app
app = web.Application( app = web.Application(
client_max_size=1024 * 1024 * 50, client_max_size=1024 * 1024 * 50,
middlewares=[security_middleware],
) # allow uploading files up to 50mb ) # allow uploading files up to 50mb
app.add_routes(routes) app.add_routes(routes)
app.add_routes( app.add_routes(
@@ -3886,7 +3924,7 @@ class ReticulumMeshChat:
"icon_name": icon_name, "icon_name": icon_name,
"foreground_colour": foreground_colour, "foreground_colour": foreground_colour,
"background_colour": background_colour, "background_colour": background_colour,
"updated_at": datetime.now(timezone.utc), "updated_at": datetime.now(UTC),
} }
# upsert to database # upsert to database
@@ -4108,7 +4146,7 @@ class ReticulumMeshChat:
"snr": lxmf_message_dict["snr"], "snr": lxmf_message_dict["snr"],
"quality": lxmf_message_dict["quality"], "quality": lxmf_message_dict["quality"],
"is_spam": is_spam, "is_spam": is_spam,
"updated_at": datetime.now(timezone.utc), "updated_at": datetime.now(UTC),
} }
# upsert to database # upsert to database
@@ -4144,7 +4182,7 @@ class ReticulumMeshChat:
"rssi": rssi, "rssi": rssi,
"snr": snr, "snr": snr,
"quality": quality, "quality": quality,
"updated_at": datetime.now(timezone.utc), "updated_at": datetime.now(UTC),
} }
# only set app data if provided, as we don't want to wipe existing data when we request keys from the network # only set app data if provided, as we don't want to wipe existing data when we request keys from the network
@@ -4170,7 +4208,7 @@ class ReticulumMeshChat:
data = { data = {
"destination_hash": destination_hash, "destination_hash": destination_hash,
"display_name": display_name, "display_name": display_name,
"updated_at": datetime.now(timezone.utc), "updated_at": datetime.now(UTC),
} }
# upsert to database # upsert to database
@@ -4193,7 +4231,7 @@ class ReticulumMeshChat:
"destination_hash": destination_hash, "destination_hash": destination_hash,
"display_name": display_name, "display_name": display_name,
"aspect": aspect, "aspect": aspect,
"updated_at": datetime.now(timezone.utc), "updated_at": datetime.now(UTC),
} }
# upsert to database # upsert to database
@@ -4210,8 +4248,8 @@ class ReticulumMeshChat:
# prepare data to insert or update # prepare data to insert or update
data = { data = {
"destination_hash": destination_hash, "destination_hash": destination_hash,
"last_read_at": datetime.now(timezone.utc), "last_read_at": datetime.now(UTC),
"updated_at": datetime.now(timezone.utc), "updated_at": datetime.now(UTC),
} }
# upsert to database # upsert to database
@@ -4878,7 +4916,7 @@ class Config:
data = { data = {
"key": key, "key": key,
"value": value, "value": value,
"updated_at": datetime.now(timezone.utc), "updated_at": datetime.now(UTC),
} }
# upsert to database # upsert to database

View File

@@ -0,0 +1 @@
"""Backend utilities shared by the Reticulum MeshChatX CLI."""

View File

@@ -3,9 +3,8 @@ import time
import RNS import RNS
from RNS.Interfaces.Interface import Interface from RNS.Interfaces.Interface import Interface
from websockets.sync.server import Server, ServerConnection, serve
from src.backend.interfaces.WebsocketClientInterface import WebsocketClientInterface from src.backend.interfaces.WebsocketClientInterface import WebsocketClientInterface
from websockets.sync.server import Server, ServerConnection, serve
class WebsocketServerInterface(Interface): class WebsocketServerInterface(Interface):

View File

@@ -0,0 +1 @@
"""Shared transport interfaces for MeshChatX."""

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

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