<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dev Shot</title>
    <description>The latest articles on DEV Community by Dev Shot (@devshotapp).</description>
    <link>https://dev.to/devshotapp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024211%2Fb1d86aa7-1310-4c9e-8c21-765038acb732.png</url>
      <title>DEV Community: Dev Shot</title>
      <link>https://dev.to/devshotapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devshotapp"/>
    <language>en</language>
    <item>
      <title>I got tired of WhatsApp-ing screenshots to myself, so I built a phone-to-PC bridge</title>
      <dc:creator>Dev Shot</dc:creator>
      <pubDate>Fri, 10 Jul 2026 14:39:23 +0000</pubDate>
      <link>https://dev.to/devshotapp/i-got-tired-of-whatsapp-ing-screenshots-to-myself-so-i-built-a-phone-to-pc-bridge-1boi</link>
      <guid>https://dev.to/devshotapp/i-got-tired-of-whatsapp-ing-screenshots-to-myself-so-i-built-a-phone-to-pc-bridge-1boi</guid>
      <description>&lt;h2&gt;
  
  
  The dumb loop
&lt;/h2&gt;

&lt;p&gt;I'm a solo dev building mobile apps with AI coding tools (Claude Code, mostly). My feedback loop looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;See a UI bug on my phone&lt;/li&gt;
&lt;li&gt;Screenshot it&lt;/li&gt;
&lt;li&gt;Send it to myself on WhatsApp&lt;/li&gt;
&lt;li&gt;Open WhatsApp Web on my PC&lt;/li&gt;
&lt;li&gt;Download the image&lt;/li&gt;
&lt;li&gt;Drag it into the AI chat&lt;/li&gt;
&lt;li&gt;Repeat 30 times a day&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every mobile dev I asked did some version of this — WhatsApp-to-self, Telegram saved messages, email drafts, USB cable and file explorer. It's a 45-second detour for something that should be instant.&lt;/p&gt;

&lt;p&gt;So I spent three weeks building DevShot: tap a floating bubble on your Android phone, and the screenshot lands on your Windows PC's clipboard over local WiFi. &lt;code&gt;Ctrl+V&lt;/code&gt; into anything. No cloud, no account.&lt;/p&gt;

&lt;p&gt;This post is about the technical decisions and the traps I fell into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phone (Kotlin) ──WebSocket/TLS──▶ Desktop (Python/PyQt6) ──▶ Clipboard
                                          │
                                          └──▶ ~/devshot/mobile-latest.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three decisions I locked in early:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native Kotlin, not Flutter/React Native.&lt;/strong&gt; Screen capture on Android means MediaProjection, and MediaProjection is painful enough without a cross-platform abstraction layer on top of it. The permission lifecycle (user grants → foreground service → projection can be revoked at any time) needs precise control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python + PyQt6 for the desktop, not Electron.&lt;/strong&gt; The desktop app is a system tray icon, a pairing window with a QR code, and a WebSocket server. Shipping a whole Chromium runtime for that felt absurd. PyInstaller gives me a single .exe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local WiFi only, no relay server.&lt;/strong&gt; This was the big one. Every "send to PC" app routes through someone's cloud. I wanted the pitch to be: &lt;em&gt;your screenshots physically cannot leave your network, because there is no server.&lt;/em&gt; Privacy as architecture, not as a policy document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that fought back
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. MediaProjection is a permission minefield
&lt;/h3&gt;

&lt;p&gt;Android (rightly) treats screen capture as radioactive. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A one-time user grant via &lt;code&gt;createScreenCaptureIntent()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A foreground service with type &lt;code&gt;mediaProjection&lt;/code&gt; running &lt;em&gt;before&lt;/em&gt; you use the token&lt;/li&gt;
&lt;li&gt;Handling for the projection being killed at any moment (battery savers on Xiaomi/Samsung do this enthusiastically)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The floating bubble itself is a &lt;code&gt;WindowManager&lt;/code&gt; overlay from a foreground service — a second permission (&lt;code&gt;SYSTEM_ALERT_WINDOW&lt;/code&gt;) with its own settings page dance.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pairing UX vs. TLS on a LAN
&lt;/h3&gt;

&lt;p&gt;I wanted encryption, but you can't get a CA-signed certificate for &lt;code&gt;192.168.1.5&lt;/code&gt;. Self-signed certs mean man-in-the-middle risk on the first connection.&lt;/p&gt;

&lt;p&gt;The fix: the desktop generates a self-signed cert on first launch and shows a &lt;strong&gt;QR code that embeds the certificate fingerprint&lt;/strong&gt; along with the IP and a pairing token. The phone scans it and pins that exact fingerprint for all future connections. The QR code becomes the trust anchor — MITM would require compromising the QR code, which is displayed on the very screen you're standing in front of.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Windows Firewall silent killer
&lt;/h3&gt;

&lt;p&gt;This one cost me a full debugging session &lt;em&gt;after&lt;/em&gt; release. If a user clicks "Cancel" on the Windows Firewall popup (instead of "Allow"), Windows creates a &lt;strong&gt;permanent Block rule&lt;/strong&gt; for your app. Not "no rule" — an active Block, and Block beats any Allow rule you add later. The phone can never connect, and nothing tells you why.&lt;/p&gt;

&lt;p&gt;The fix was making the installer register the firewall rule itself during setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;netsh&lt;/span&gt; &lt;span class="kd"&gt;advfirewall&lt;/span&gt; &lt;span class="kd"&gt;firewall&lt;/span&gt; &lt;span class="kd"&gt;add&lt;/span&gt; &lt;span class="kd"&gt;rule&lt;/span&gt; &lt;span class="kd"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"DevShot"&lt;/span&gt; &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kd"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kd"&gt;allow&lt;/span&gt; &lt;span class="kd"&gt;program&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"...\DevShot-GUI.exe"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Inno Setup, elevated via ShellExec &lt;code&gt;runas&lt;/code&gt; since the installer itself runs unprivileged.) If you ship anything with a listening socket on Windows, do this on day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The filesystem is an API
&lt;/h3&gt;

&lt;p&gt;Besides the clipboard, every screenshot is written to a predictable path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/devshot/
├── mobile-latest.png      ← always the newest phone screenshot
├── mobile-latest.json     ← metadata (device, resolution, timestamp)
└── mobile-history/        ← one file per capture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turned out to be the feature AI-tool users care about most. CLI agents can just read &lt;code&gt;~/devshot/mobile-latest.png&lt;/code&gt; by path — no integration, no plugin, no MCP server. A stable file path is the most universal API there is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately didn't build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iOS/Mac support&lt;/strong&gt; — v2, maybe. Solo dev, fixed scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-network / internet mode&lt;/strong&gt; — would need a relay server, which breaks the entire privacy model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounts&lt;/strong&gt; — license keys via LemonSqueezy for the Pro tier, validated on-device. No user database anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics SDK&lt;/strong&gt; — there's no telemetry in the app at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cutting scope is what let me ship in three weeks instead of never.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Free tier does 15 screenshots/day; Pro ($9/mo) is unlimited with multi-device. Windows + Android today.&lt;/p&gt;

&lt;p&gt;Honest caveats: the installer is unsigned (code-signing certs are $200+/yr — SmartScreen will warn you), and the site is still on a vercel.app subdomain. Solo-dev life.&lt;/p&gt;

&lt;p&gt;Site: &lt;a href="https://devshot-app.vercel.app" rel="noopener noreferrer"&gt;https://devshot-app.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd genuinely love feedback from this community — especially on the QR-pinned-TLS approach, and whether the free/Pro split feels fair. Ask me anything about MediaProjection scars in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>android</category>
      <category>python</category>
      <category>kotlin</category>
    </item>
  </channel>
</rss>
