Browse Source

linux server prompt user for presentation

xdw 2 months ago
parent
commit
e900e800b4
3 changed files with 161 additions and 4 deletions
  1. 60 2
      README.md
  2. 33 2
      ppt_client/lib/slide_controller.dart
  3. 68 0
      server/index.ts

+ 60 - 2
README.md

@@ -1,6 +1,6 @@
 # PPT Remote
 
-Control PowerPoint from your Android phone over Wi-Fi.
+Control PowerPoint or LibreOffice Impress from your phone over Wi-Fi.
 
 ## Server (Windows PC)
 
@@ -43,13 +43,71 @@ Find your machine's local IP with `ip addr` or `hostname -I`.
 Requirements: Flutter SDK.
 
 ```bash
-cd client
+cd ppt_client
 flutter pub get
 flutter run
 ```
 
 Enter your PC's local IP and port `8765`, then tap Connect.
 
+To build a release APK:
+
+```bash
+flutter build apk --release
+# output: build/app/outputs/flutter-apk/app-release.apk
+```
+
+## Client (iOS)
+
+Requirements: macOS with Xcode 14+, Flutter SDK, an Apple Developer account (free account works for direct device install via USB).
+
+**1. Install dependencies**
+
+```bash
+cd ppt_client
+flutter pub get
+```
+
+**2. Open the iOS project in Xcode to set up signing**
+
+```bash
+open ios/Runner.xcworkspace
+```
+
+In Xcode:
+- Select the `Runner` target → `Signing & Capabilities`
+- Set your Team (Apple ID)
+- Change the Bundle Identifier to something unique, e.g. `com.yourname.pptremote`
+
+**3. Allow plain WebSocket (ws://) traffic**
+
+iOS also blocks cleartext HTTP/WS by default. Add an exception in `ios/Runner/Info.plist`:
+
+```xml
+<key>NSAppTransportSecurity</key>
+<dict>
+    <key>NSAllowsArbitraryLoads</key>
+    <true/>
+</dict>
+```
+
+**4. Run on a connected iPhone/iPad**
+
+```bash
+flutter run
+```
+
+Or build an IPA for distribution:
+
+```bash
+flutter build ipa
+# output: build/ios/ipa/ppt_remote.ipa
+```
+
+To install the IPA without the App Store, use [AltStore](https://altstore.io) or Apple Configurator 2, or just use `flutter run --release` with the device connected via USB.
+
+> Note: A paid Apple Developer account ($99/year) is required to distribute via TestFlight or the App Store. A free account lets you sideload directly to your own device for 7 days at a time.
+
 ## How it works
 
 - Server auto-detects the platform (`process.platform`) and picks the right driver.

+ 33 - 2
ppt_client/lib/slide_controller.dart

@@ -25,6 +25,7 @@ class _RemoteScreenState extends State<RemoteScreen> {
   int _total = 0;
   Uint8List? _slideImage;
   bool _connected = false;
+  bool _presentationOpen = true; // assume open on Windows; Linux server will correct
   String? _error;
 
   @override
@@ -67,12 +68,28 @@ class _RemoteScreenState extends State<RemoteScreen> {
       setState(() {
         _current = (msg['current'] as num).toInt();
         _total = (msg['total'] as num).toInt();
+        _presentationOpen = true;
       });
     } else if (event == 'image') {
       final b64 = msg['data'] as String?;
       if (b64 != null && b64.isNotEmpty) {
         setState(() => _slideImage = base64Decode(b64));
       }
+    } else if (event == 'presentation') {
+      final status = msg['status'] as String?;
+      setState(() {
+        _presentationOpen = status == 'opened';
+        if (!_presentationOpen) {
+          _slideImage = null;
+          _current = 0;
+          _total = 0;
+        }
+      });
+      if (status == 'closed') {
+        ScaffoldMessenger.of(context).showSnackBar(
+          const SnackBar(content: Text('Presentation closed — waiting for new file...')),
+        );
+      }
     } else if (event == 'error') {
       ScaffoldMessenger.of(context).showSnackBar(
         SnackBar(content: Text(msg['message'] ?? 'Unknown error')),
@@ -124,13 +141,27 @@ class _RemoteScreenState extends State<RemoteScreen> {
                     child: Column(
                       mainAxisSize: MainAxisSize.min,
                       children: [
-                        const Icon(Icons.slideshow, size: 80, color: Colors.white24),
+                        Icon(
+                          _presentationOpen ? Icons.slideshow : Icons.hourglass_empty,
+                          size: 80,
+                          color: Colors.white24,
+                        ),
                         const SizedBox(height: 12),
                         Text(
-                          _error ?? 'No slide preview',
+                          _presentationOpen
+                              ? (_error ?? 'No slide preview')
+                              : 'Waiting for presentation\nto be opened on server...',
                           style: const TextStyle(color: Colors.white38),
                           textAlign: TextAlign.center,
                         ),
+                        if (!_presentationOpen) ...[
+                          const SizedBox(height: 16),
+                          const SizedBox(
+                            width: 24,
+                            height: 24,
+                            child: CircularProgressIndicator(strokeWidth: 2),
+                          ),
+                        ],
                       ],
                     ),
                   ),

+ 68 - 0
server/index.ts

@@ -249,6 +249,70 @@ except Exception as e:
   },
 };
 
+// ─── Linux: LibreOffice process manager ──────────────────────────────────────
+// Prompts the user to pick a presentation file via zenity (GTK file dialog),
+// launches LibreOffice with the UNO socket listener, and re-prompts when
+// the process exits.
+
+const UNO_PORT = 2002;
+let libreofficeProc: ReturnType<typeof Bun.spawn> | null = null;
+
+async function pickFile(): Promise<string | null> {
+  try {
+    const result = await run("zenity", [
+      "--file-selection",
+      "--title=Open Presentation",
+    ]);
+    return result || null;
+  } catch {
+    // zenity not available — fall back to terminal prompt
+    process.stdout.write("Enter path to presentation file: ");
+    for await (const line of console) {
+      return line.trim() || null;
+    }
+    return null;
+  }
+}
+
+async function launchLibreOffice(filePath: string): Promise<void> {
+  console.log(`Opening: ${filePath}`);
+  libreofficeProc = Bun.spawn([
+    "libreoffice",
+    `--accept=socket,host=localhost,port=${UNO_PORT};urp;StarOffice.ServiceManager`,
+    "--impress",
+    filePath,
+  ], { stdout: "inherit", stderr: "inherit" });
+
+  // Wait for UNO socket to become available (up to 10s)
+  for (let i = 0; i < 20; i++) {
+    await Bun.sleep(500);
+    try {
+      const status = await LinuxDriver.status();
+      if (status.total > 0) break;
+    } catch { /* not ready yet */ }
+  }
+
+  broadcast({ event: "presentation", status: "opened", file: filePath.split("/").pop() });
+
+  // Watch for process exit, then re-prompt
+  libreofficeProc.exited.then(async () => {
+    console.log("LibreOffice closed.");
+    broadcast({ event: "presentation", status: "closed" });
+    libreofficeProc = null;
+    await promptAndLaunch();
+  });
+}
+
+async function promptAndLaunch(): Promise<void> {
+  const file = await pickFile();
+  if (!file) {
+    console.log("No file selected. Waiting... (clients will be notified when a file is opened)");
+    broadcast({ event: "presentation", status: "none" });
+    return;
+  }
+  await launchLibreOffice(file);
+}
+
 // ─── Select driver based on platform ─────────────────────────────────────────
 
 const driver: Driver = IS_LINUX ? LinuxDriver : WindowsDriver;
@@ -333,3 +397,7 @@ const server = Bun.serve({
 });
 
 console.log(`PPT Remote Server listening on ws://0.0.0.0:${PORT}`);
+
+// On Linux, prompt to open a presentation immediately
+if (IS_LINUX) promptAndLaunch();
+else console.log("Make sure PowerPoint is open with a presentation before connecting.");