Compare commits

...

10 Commits

Author SHA1 Message Date
陈大猫
a27b99cbf7 Merge pull request #79 from Nightsuki/fix/ssh-config-import-file-filter
Some checks failed
build-packages / build-macos-latest (push) Has been cancelled
build-packages / build-ubuntu-latest (push) Has been cancelled
build-packages / build-windows-latest (push) Has been cancelled
build-packages / release (push) Has been cancelled
fix: allow importing ssh_config files without extension
2026-01-18 18:26:52 +08:00
Nightsuki
3d6e981758 fix: allow importing ssh_config files without extension
The file picker filter for ssh_config import was set to only accept
.conf, .config, and .txt files. However, the standard SSH config file
at ~/.ssh/config has no file extension, making it impossible to select
in the import dialog.

Changed the accept attribute to '*' to allow selecting any file,
including extension-less files like 'config'.
2026-01-18 18:20:39 +08:00
bincxz
e6d8c1381c Updates README screenshots
Replaces multiple screenshots with a single, more representative image across all language versions of the README files.
2026-01-17 21:28:33 +08:00
bincxz
bc3d73c683 Refreshes READMEs with latest features and screenshots
Updates the project READMEs across all languages to reflect recent feature additions and improvements.

*   Adds a direct link to the official application website.
*   Refreshes the Vault section with new images showcasing grid view, nested folder organization, and list view.
*   Updates the Terminal section to highlight Broadcast Mode and performance monitoring, replacing older screenshots.
*   Enhances the SFTP section with an updated dual-pane view and a new screenshot for the transfer queue.
*   Introduces a screenshot for the new Key Generator feature in the Keychain section.
*   Adds a "Contributors" section to acknowledge community contributions.
2026-01-17 21:26:36 +08:00
bincxz
dd5f3ddffd Ignores Monaco editor public assets
Prevents ESLint from processing files within the `public/monaco` directory. This helps avoid linting third-party or generated code, reducing unnecessary warnings and improving linting performance.
2026-01-17 17:45:58 +08:00
陈大猫
3959328e24 Merge pull request #77 from binaricat/copilot/add-sftp-reconnect-feature
Some checks failed
build-packages / build-macos-latest (push) Has been cancelled
build-packages / build-ubuntu-latest (push) Has been cancelled
build-packages / build-windows-latest (push) Has been cancelled
build-packages / release (push) Has been cancelled
Add SFTP reconnect UI overlay with spinner
2026-01-17 04:12:37 +08:00
bincxz
48928254fa Adds auto-reconnect for lost SFTP sessions
Detects common SFTP session errors and automatically attempts to re‑establish the connection (up to three tries).
Provides user feedback with a reconnect overlay, spinner integration, and success/error toast notifications.
Adds corresponding English and Chinese i18n messages for reconnect status and failure.
Minor build config comment added (no functional impact).
2026-01-17 04:08:06 +08:00
copilot-swe-agent[bot]
30962c992f Revert package-lock.json changes
Co-authored-by: binaricat <16399091+binaricat@users.noreply.github.com>
2026-01-16 11:32:35 +00:00
copilot-swe-agent[bot]
02e0fae051 Add reconnecting overlay UI with spinner for SFTP connections
Co-authored-by: binaricat <16399091+binaricat@users.noreply.github.com>
2026-01-16 11:30:01 +00:00
copilot-swe-agent[bot]
6a94716880 Initial plan 2026-01-16 11:24:06 +00:00
28 changed files with 267 additions and 36 deletions

View File

@@ -5,7 +5,8 @@
<h1 align="center">Netcatty</h1>
<p align="center">
<strong>モダンな SSH クライアント、SFTP ブラウザ & ターミナルマネージャー</strong>
<strong>モダンな SSH クライアント、SFTP ブラウザ & ターミナルマネージャー</strong><br/>
<a href="https://netcatty.app"><strong>netcatty.app</strong></a>
</p>
<p align="center">
@@ -39,7 +40,7 @@
---
[![Netcatty メインインターフェース](screenshots/main-window-dark.png)](screenshots/main-window-dark.png)
[![Netcatty メインインターフェース](screenshots/vault_grid_view.png)](screenshots/vault_grid_view.png)
---
@@ -138,15 +139,15 @@ Vault ビューはすべての SSH 接続を管理するコマンドセンター
**ダークモード**
![ダークモード](screenshots/main-window-dark.png)
![ホスト管理](screenshots/vault_grid_view.png)
**ライトモード**
**ネストされたフォルダと整理**
![ライトモード](screenshots/main-window-light.png)
![ネストされたフォルダ](screenshots/nested_folder_structure.png)
**リストビュー**
![リストビュー](screenshots/main-window-dark-list.png)
![リストビュー](screenshots/vault_list_view.png)
<a name="ターミナル"></a>
## ターミナル
@@ -155,18 +156,28 @@ WebGL アクセラレーション対応の xterm.js ベースのターミナル
**分割ウィンドウ**
![分割ウィンドウ](screenshots/split-window.png)
**ブロードキャストモード**
**テーマカスタマイズ**
一度入力すれば、どこでも実行できます。複数のサーバーを同時にメンテナンスするのに最適です。
![テーマカスタマイズ](screenshots/terminal-theme-change.png)
![ブロードキャストモード](screenshots/broadcast_mode.png)
**パフォーマンス情報とカスタマイズ**
接続の健全性を監視し、ターミナルのあらゆる側面をカスタマイズします。
![ターミナルパフォーマンス](screenshots/terminal_performance.png)
<a name="sftp"></a>
## SFTP
デュアルペイン SFTP ブラウザは、ローカルからリモート、リモートからリモートへのファイル転送をサポート。シングルクリックでディレクトリを移動、ペイン間でファイルをドラッグ&ドロップ、転送進捗をリアルタイムで監視。インターフェースにはファイル権限、サイズ、変更日時を表示。複数の転送をキューに入れ、詳細な速度と進捗インジケーターで完了を確認。コンテキストメニューから名前変更、削除、ダウンロード、アップロード操作にすばやくアクセス。
![SFTP ビュー](screenshots/sftp.png)
![SFTP デュアルペイン](screenshots/sftp_dual_pane.png)
**転送キュー**
![転送キュー](screenshots/sftp_transfer_queue.png)
<a name="キーチェーン"></a>
## キーチェーン
@@ -188,6 +199,10 @@ WebGL アクセラレーション対応の xterm.js ベースのターミナル
![キーマネージャー](screenshots/key-manager.png)
**キー生成**
![キー生成](screenshots/key_generator_ui.png)
<a name="ポートフォワーディング"></a>
## ポートフォワーディング
@@ -365,6 +380,17 @@ npm run pack:linux # Linux (AppImage, deb, rpm)
---
<a name="コントリビューター"></a>
# コントリビューター
貢献してくれたすべての人々に感謝します!
<a href="https://github.com/binaricat/Netcatty/graphs/contributors">
<img src="https://contrib.rocks/image?repo=binaricat/Netcatty" alt="contributors" />
</a>
---
<a name="ライセンス"></a>
# ライセンス

View File

@@ -5,7 +5,8 @@
<h1 align="center">Netcatty</h1>
<p align="center">
<strong>Modern SSH Client, SFTP Browser & Terminal Manager</strong>
<strong>Modern SSH Client, SFTP Browser & Terminal Manager</strong><br/>
<a href="https://netcatty.app"><strong>netcatty.app</strong></a>
</p>
<p align="center">
@@ -39,7 +40,7 @@
---
[![Netcatty Main Interface](screenshots/main-window-dark.png)](screenshots/main-window-dark.png)
[![Netcatty Main Interface](screenshots/vault_grid_view.png)](screenshots/vault_grid_view.png)
---
@@ -138,15 +139,15 @@ The Vault view is your command center for managing all SSH connections. Create h
**Dark Mode**
![Dark Mode](screenshots/main-window-dark.png)
![Host Management](screenshots/vault_grid_view.png)
**Light Mode**
**Nested Folders & Organization**
![Light Mode](screenshots/main-window-light.png)
![Nested Folders](screenshots/nested_folder_structure.png)
**List View**
![List View](screenshots/main-window-dark-list.png)
![List View](screenshots/vault_list_view.png)
<a name="terminal"></a>
## Terminal
@@ -155,18 +156,28 @@ Powered by xterm.js with WebGL acceleration, the terminal delivers a smooth, res
**Split Windows**
![Split Windows](screenshots/split-window.png)
**Broadcast Mode**
**Theme Customization**
Type once, execute everywhere. Great for maintaining multiple servers simultaneously.
![Theme Customization](screenshots/terminal-theme-change.png)
![Broadcast Mode](screenshots/broadcast_mode.png)
**Performance Info & Customization**
Monitor your connection health and customize every aspect of your terminal.
![Terminal Performance](screenshots/terminal_performance.png)
<a name="sftp"></a>
## SFTP
The dual-pane SFTP browser supports local-to-remote and remote-to-remote file transfers. Navigate directories with single-click, drag files between panes, and monitor transfer progress in real-time. The interface shows file permissions, sizes, and modification dates. Queue multiple transfers and watch them complete with detailed speed and progress indicators. Context menus provide quick access to rename, delete, download, and upload operations.
![SFTP View](screenshots/sftp.png)
![SFTP Dual Pane](screenshots/sftp_dual_pane.png)
**Transfer Queue**
![Transfer Queue](screenshots/sftp_transfer_queue.png)
<a name="keychain"></a>
## Keychain
@@ -188,6 +199,10 @@ The Keychain is your secure vault for SSH credentials. Generate new keys, import
![Key Manager](screenshots/key-manager.png)
**Key Generator**
![Key Generator](screenshots/key_generator_ui.png)
<a name="port-forwarding"></a>
## Port Forwarding
@@ -365,6 +380,17 @@ See [agents.md](agents.md) for architecture overview and coding conventions.
---
<a name="contributors"></a>
# Contributors
Thanks to all the people who contribute!
<a href="https://github.com/binaricat/Netcatty/graphs/contributors">
<img src="https://contrib.rocks/image?repo=binaricat/Netcatty" alt="contributors" />
</a>
---
<a name="license"></a>
# License

View File

@@ -5,7 +5,8 @@
<h1 align="center">Netcatty</h1>
<p align="center">
<strong>现代化 SSH 客户端、SFTP 浏览器 & 终端管理器</strong>
<strong>现代化 SSH 客户端、SFTP 浏览器 & 终端管理器</strong><br/>
<a href="https://netcatty.app"><strong>netcatty.app</strong></a>
</p>
<p align="center">
@@ -39,7 +40,7 @@
---
[![Netcatty 主界面](screenshots/main-window-dark.png)](screenshots/main-window-dark.png)
[![Netcatty 主界面](screenshots/vault_grid_view.png)](screenshots/vault_grid_view.png)
---
@@ -138,15 +139,15 @@ Vault 视图是管理所有 SSH 连接的控制中心。通过右键菜单创建
**深色模式**
![深色模式](screenshots/main-window-dark.png)
![主机管理](screenshots/vault_grid_view.png)
**浅色模式**
**层级文件夹与分组**
![浅色模式](screenshots/main-window-light.png)
![层级文件夹](screenshots/nested_folder_structure.png)
**列表视图**
![列表视图](screenshots/main-window-dark-list.png)
![列表视图](screenshots/vault_list_view.png)
<a name="终端"></a>
## 终端
@@ -155,18 +156,28 @@ Vault 视图是管理所有 SSH 连接的控制中心。通过右键菜单创建
**分屏窗口**
![分屏窗口](screenshots/split-window.png)
**广播模式**
**主题定制**
一次输入,多处执行。非常适合同时维护这多台服务器。
![主题定制](screenshots/terminal-theme-change.png)
![广播模式](screenshots/broadcast_mode.png)
**性能信息与定制**
监控连接健康状况,并自定义终端的方方面面。
![终端性能](screenshots/terminal_performance.png)
<a name="sftp"></a>
## SFTP
双窗格 SFTP 浏览器支持本地到远程和远程到远程的文件传输。单击导航目录,在窗格之间拖放文件,实时监控传输进度。界面显示文件权限、大小和修改日期。批量传输队列管理,详细的速度和进度指示器。右键菜单快速访问重命名、删除、下载和上传操作。
![SFTP 视图](screenshots/sftp.png)
![SFTP 双窗格](screenshots/sftp_dual_pane.png)
**传输队列**
![传输队列](screenshots/sftp_transfer_queue.png)
<a name="密钥管理"></a>
## 密钥管理
@@ -188,6 +199,10 @@ Vault 视图是管理所有 SSH 连接的控制中心。通过右键菜单创建
![密钥管理器](screenshots/key-manager.png)
**密钥生成器**
![密钥生成器](screenshots/key_generator_ui.png)
<a name="端口转发"></a>
## 端口转发
@@ -365,6 +380,17 @@ npm run pack:linux # Linux (AppImage, deb, rpm)
---
<a name="贡献者"></a>
# 贡献者
感谢所有参与贡献的人!
<a href="https://github.com/binaricat/Netcatty/graphs/contributors">
<img src="https://contrib.rocks/image?repo=binaricat/Netcatty" alt="contributors" />
</a>
---
<a name="开源协议"></a>
# 开源协议

View File

@@ -542,6 +542,12 @@ const en: Messages = {
'sftp.autoSync.success': 'File synced to remote: {fileName}',
'sftp.autoSync.error': 'Failed to sync file: {error}',
// SFTP Reconnecting
'sftp.reconnecting.title': 'Reconnecting...',
'sftp.reconnecting.desc': 'Connection lost, attempting to reconnect',
'sftp.reconnected': 'Connection restored',
'sftp.error.reconnectFailed': 'Failed to reconnect. Please try again.',
// Settings > SFTP Show Hidden Files
'settings.sftp.showHiddenFiles': 'Show hidden files',
'settings.sftp.showHiddenFiles.desc': 'Display files with the Windows hidden attribute in the SFTP file browser when browsing local Windows filesystem.',

View File

@@ -780,6 +780,12 @@ const zhCN: Messages = {
'sftp.autoSync.success': '文件已同步到远程:{fileName}',
'sftp.autoSync.error': '同步文件失败:{error}',
// SFTP Reconnecting
'sftp.reconnecting.title': '正在重连...',
'sftp.reconnecting.desc': '连接已断开,正在尝试重新连接',
'sftp.reconnected': '连接已恢复',
'sftp.error.reconnectFailed': '重连失败,请重试。',
// Settings > SFTP Show Hidden Files
'settings.sftp.showHiddenFiles': '显示隐藏文件',
'settings.sftp.showHiddenFiles.desc': '在浏览本地 Windows 文件系统时,显示具有 Windows 隐藏属性的文件。',

View File

@@ -325,6 +325,12 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
const lastSelectedIndexRef = useRef<number | null>(null);
const localHomeRef = useRef<string | null>(null);
// Reconnect state
const [reconnecting, setReconnecting] = useState(false);
const reconnectingRef = useRef(false);
const reconnectAttemptsRef = useRef(0);
const MAX_RECONNECT_ATTEMPTS = 3;
// Rename dialog state
const [showRenameDialog, setShowRenameDialog] = useState(false);
const [renameTarget, setRenameTarget] = useState<RemoteFile | null>(null);
@@ -536,6 +542,40 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
openSftp,
]);
// Check if an error indicates a stale/lost SFTP session
const isSessionError = useCallback((err: unknown): boolean => {
if (!(err instanceof Error)) return false;
const msg = err.message.toLowerCase();
return (
msg.includes("session not found") ||
msg.includes("sftp session") ||
msg.includes("not found") ||
msg.includes("closed") ||
msg.includes("connection reset") ||
msg.includes("eof")
);
}, []);
// Handle session error - triggers auto-reconnect
const handleSessionError = useCallback(() => {
if (reconnectingRef.current) return; // Prevent duplicate reconnect attempts
if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) {
toast.error(t("sftp.error.reconnectFailed"), "SFTP");
setReconnecting(false);
reconnectingRef.current = false;
return;
}
// Clear stale session reference
sftpIdRef.current = null;
// Set reconnecting state
reconnectingRef.current = true;
reconnectAttemptsRef.current++;
setReconnecting(true);
}, [t]);
const loadFiles = useCallback(
async (path: string, options?: { force?: boolean }) => {
const requestId = ++loadSeqRef.current;
@@ -569,6 +609,14 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
setSelectedFiles(new Set());
} catch (e) {
if (loadSeqRef.current !== requestId) return;
// Check if this is a session error that can trigger auto-reconnect
if (!isLocalSession && isSessionError(e) && files.length > 0) {
logger.info("[SFTP] Session lost, attempting to reconnect...");
handleSessionError();
return;
}
logger.error("Failed to load files", e);
toast.error(
e instanceof Error ? e.message : t("sftp.error.loadFailed"),
@@ -581,7 +629,7 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
}
}
},
[ensureSftp, host.id, isLocalSession, listLocalDir, listSftp, t],
[ensureSftp, host.id, isLocalSession, listLocalDir, listSftp, t, isSessionError, handleSessionError, files.length],
);
useLayoutEffect(() => {
@@ -615,6 +663,71 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
};
}, [closeSftpSession]);
// Auto-reconnect effect
useEffect(() => {
if (!reconnecting || !reconnectingRef.current || isLocalSession) return;
const attemptReconnect = async () => {
// Small delay before reconnecting
await new Promise((resolve) => setTimeout(resolve, 1000));
if (!reconnectingRef.current) return; // May have been cancelled
try {
// Re-establish SFTP connection
const sftpId = await openSftp({
sessionId: `sftp-modal-${host.id}`,
hostname: credentials.hostname,
username: credentials.username || "root",
port: credentials.port || 22,
password: credentials.password,
privateKey: credentials.privateKey,
certificate: credentials.certificate,
passphrase: credentials.passphrase,
publicKey: credentials.publicKey,
keyId: credentials.keyId,
keySource: credentials.keySource,
proxy: credentials.proxy,
jumpHosts: credentials.jumpHosts,
});
sftpIdRef.current = sftpId;
// Refresh current directory
const list = await listSftp(sftpId, currentPath);
dirCacheRef.current.set(`${host.id}::${currentPath}`, {
files: list,
timestamp: Date.now(),
});
setFiles(list);
setSelectedFiles(new Set());
// Reconnect successful
reconnectingRef.current = false;
reconnectAttemptsRef.current = 0;
setReconnecting(false);
toast.success(t("sftp.reconnected"), "SFTP");
} catch (e) {
logger.error("[SFTP] Reconnect failed", e);
// Check if we can retry
if (reconnectAttemptsRef.current < MAX_RECONNECT_ATTEMPTS) {
// Trigger another attempt
sftpIdRef.current = null;
reconnectingRef.current = false; // Reset to allow handleSessionError to work
handleSessionError();
} else {
// Max retries reached
reconnectingRef.current = false;
setReconnecting(false);
toast.error(t("sftp.error.reconnectFailed"), "SFTP");
}
} finally {
setLoading(false);
}
};
attemptReconnect();
}, [reconnecting, isLocalSession, host.id, credentials, openSftp, listSftp, currentPath, t, handleSessionError]);
useEffect(() => {
if (open) {
// Check if we need to reinitialize (either first time or initialPath changed)
@@ -1703,7 +1816,7 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
className="h-7 w-7"
onClick={() => loadFiles(currentPath, { force: true })}
>
<RefreshCw size={14} className={cn(loading && "animate-spin")} />
<RefreshCw size={14} className={cn((loading || reconnecting) && "animate-spin")} />
</Button>
{/* Editable Breadcrumbs */}
@@ -1897,6 +2010,19 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
</div>
)}
{/* Reconnecting overlay */}
{reconnecting && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-20">
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-secondary/90 border border-border/60 shadow-lg">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<div className="text-center">
<div className="text-sm font-medium">{t("sftp.reconnecting.title")}</div>
<div className="text-xs text-muted-foreground mt-1">{t("sftp.reconnecting.desc")}</div>
</div>
</div>
</div>
)}
{files.length === 0 && !loading && (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Folder size={48} className="mb-3 opacity-50" />

View File

@@ -1291,7 +1291,7 @@ const SftpPaneViewInner: React.FC<SftpPaneViewProps> = ({
<div className="flex items-center justify-center h-full">
<Loader2 size={24} className="animate-spin text-muted-foreground" />
</div>
) : pane.error ? (
) : pane.error && !pane.reconnecting ? (
<div className="flex flex-col items-center justify-center h-full gap-2 text-destructive">
<AlertCircle size={24} />
<span className="text-sm">{pane.error}</span>
@@ -1341,12 +1341,25 @@ const SftpPaneViewInner: React.FC<SftpPaneViewProps> = ({
</div>
{/* Loading overlay - covers entire pane when navigating directories */}
{pane.loading && sortedDisplayFiles.length > 0 && (
{pane.loading && sortedDisplayFiles.length > 0 && !pane.reconnecting && (
<div className="absolute inset-0 flex items-center justify-center bg-background/40 backdrop-blur-[1px] pointer-events-none z-10">
<Loader2 size={24} className="animate-spin text-muted-foreground" />
</div>
)}
{/* Reconnecting overlay - shows when SFTP connection is lost and reconnecting */}
{pane.reconnecting && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-20">
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-secondary/90 border border-border/60 shadow-lg">
<Loader2 size={32} className="animate-spin text-primary" />
<div className="text-center">
<div className="text-sm font-medium">{t("sftp.reconnecting.title")}</div>
<div className="text-xs text-muted-foreground mt-1">{t("sftp.reconnecting.desc")}</div>
</div>
</div>
</div>
)}
{/* Dialogs */}
<Dialog open={showNewFolderDialog} onOpenChange={setShowNewFolderDialog}>
<DialogContent className="max-w-sm">

View File

@@ -47,7 +47,7 @@ const OPTIONS: ImportOption[] = [
format: "ssh_config",
label: "ssh_config",
iconSrc: "/import/file.png",
accept: ".conf,.config,.txt",
accept: "*",
},
];

View File

@@ -1,3 +1,4 @@
/* global __dirname */
const path = require('path');
/**

View File

@@ -7,7 +7,7 @@ import reactHooks from "eslint-plugin-react-hooks";
export default [
js.configs.recommended,
{
ignores: ["node_modules/**", "dist/**", "electron/**", "scripts/**"],
ignores: ["node_modules/**", "dist/**", "electron/**", "scripts/**", "public/monaco/**"],
},
{
files: ["**/*.{ts,tsx}"],

1
package-lock.json generated
View File

@@ -7212,6 +7212,7 @@
"integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "26.4.0",
"builder-util": "26.3.4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB