viewsync-master 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/index.js +49 -0
- package/package.json +14 -0
- package/viewsync_helper.py +28 -0
- package/viewsync_main.py +275 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ViewSync Master 🚀 (v1.0.0)
|
|
2
|
+
|
|
3
|
+
A powerful, lag-free mirroring tool and live cropper for Android devices! Perfect for streaming study lectures seamlessly on your PC.
|
|
4
|
+
|
|
5
|
+
## ✨ Features
|
|
6
|
+
- **Live Cropper:** Remove black bars on the fly and save unlimited profiles.
|
|
7
|
+
- **Lag-Free Streaming:** Optimized bitrate (2Mbps) and scaled resolution (1024px) for buttery smooth 30fps playback.
|
|
8
|
+
- **Hardware Keyboard Shortcuts:** Use your laptop keyboard to control media!
|
|
9
|
+
- `Spacebar` = Play / Pause
|
|
10
|
+
- `Right Arrow` = Fast Forward (10s)
|
|
11
|
+
- `Left Arrow` = Rewind (10s)
|
|
12
|
+
- **1-Click Launch:** Beautiful dark-mode UI.
|
|
13
|
+
|
|
14
|
+
## 📥 Installation
|
|
15
|
+
|
|
16
|
+
Simply run this command anywhere in your terminal:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx viewsync-master
|
|
20
|
+
```
|
|
21
|
+
OR
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g viewsync-master
|
|
24
|
+
viewsync-master
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
*(Note: During installation, it will ask for your sudo password to install background dependencies like adb and xdotool.)*
|
|
28
|
+
|
|
29
|
+
## 🚀 Usage
|
|
30
|
+
1. Connect your Android phone to your PC via USB (ensure USB Debugging is enabled).
|
|
31
|
+
2. Double-click the **ViewSync Master** icon on your Desktop.
|
|
32
|
+
3. Adjust the sliders to crop the screen perfectly, save your profile, and click **Launch**.
|
package/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
console.log("🚀 Welcome to ViewSync Master Installer!");
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
console.log("Installing system dependencies (requires sudo password)...");
|
|
12
|
+
execSync('sudo apt update && sudo apt install -y scrcpy adb python3-tk xdotool python3-pip', { stdio: 'inherit' });
|
|
13
|
+
|
|
14
|
+
console.log("Installing Python packages...");
|
|
15
|
+
try {
|
|
16
|
+
execSync('pip3 install pynput --break-system-packages', { stdio: 'ignore' });
|
|
17
|
+
} catch (e) {
|
|
18
|
+
execSync('pip3 install pynput', { stdio: 'ignore' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const targetDir = path.join(os.homedir(), '.viewsync-master');
|
|
22
|
+
if (!fs.existsSync(targetDir)) {
|
|
23
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log("Setting up ViewSync Master files...");
|
|
27
|
+
fs.copyFileSync(path.join(__dirname, 'viewsync_main.py'), path.join(targetDir, 'viewsync_main.py'));
|
|
28
|
+
fs.copyFileSync(path.join(__dirname, 'viewsync_helper.py'), path.join(targetDir, 'viewsync_helper.py'));
|
|
29
|
+
|
|
30
|
+
console.log("Creating Desktop Shortcut...");
|
|
31
|
+
const desktopDir = path.join(os.homedir(), 'Desktop');
|
|
32
|
+
const shortcutPath = path.join(desktopDir, 'ViewSync Master.desktop');
|
|
33
|
+
|
|
34
|
+
const desktopEntry = `[Desktop Entry]
|
|
35
|
+
Name=ViewSync Master
|
|
36
|
+
Comment=Live screen cropper & lag-free mirroring
|
|
37
|
+
Exec=bash -c "python3 ~/.viewsync-master/viewsync_main.py"
|
|
38
|
+
Icon=phone
|
|
39
|
+
Terminal=false
|
|
40
|
+
Type=Application
|
|
41
|
+
Categories=Utility;
|
|
42
|
+
`;
|
|
43
|
+
fs.writeFileSync(shortcutPath, desktopEntry);
|
|
44
|
+
fs.chmodSync(shortcutPath, 0o755);
|
|
45
|
+
|
|
46
|
+
console.log("✅ Installation Complete! You can now launch 'ViewSync Master' from your Desktop.");
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("❌ Installation failed:", error.message);
|
|
49
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "viewsync-master",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Live screen cropper, lag-free mirroring & hardware keyboard shortcuts.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"viewsync-master": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"author": "Anurag",
|
|
13
|
+
"license": "ISC"
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import time
|
|
3
|
+
from pynput import keyboard
|
|
4
|
+
|
|
5
|
+
def get_active_window_title():
|
|
6
|
+
try:
|
|
7
|
+
window_id = subprocess.check_output(['xdotool', 'getactivewindow']).strip()
|
|
8
|
+
window_name = subprocess.check_output(['xdotool', 'getwindowname', window_id]).decode('utf-8', errors='ignore').strip()
|
|
9
|
+
return window_name.lower()
|
|
10
|
+
except Exception:
|
|
11
|
+
return ""
|
|
12
|
+
|
|
13
|
+
def on_press(key):
|
|
14
|
+
try:
|
|
15
|
+
title = get_active_window_title()
|
|
16
|
+
if "viewsync" in title:
|
|
17
|
+
if key == keyboard.Key.space:
|
|
18
|
+
subprocess.Popen(["/usr/bin/adb", "shell", "input", "keyevent", "85"])
|
|
19
|
+
elif key == keyboard.Key.right:
|
|
20
|
+
subprocess.Popen(["/usr/bin/adb", "shell", "input", "keyevent", "90"])
|
|
21
|
+
elif key == keyboard.Key.left:
|
|
22
|
+
subprocess.Popen(["/usr/bin/adb", "shell", "input", "keyevent", "89"])
|
|
23
|
+
except Exception as e:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
with keyboard.Listener(on_press=on_press) as listener:
|
|
28
|
+
listener.join()
|
package/viewsync_main.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import ttk, messagebox, simpledialog
|
|
3
|
+
import subprocess
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
CONFIG_FILE = os.path.join(os.path.expanduser('~'), '.viewsync_profiles.json')
|
|
8
|
+
OLD_CONFIG = os.path.join(os.path.expanduser('~'), '.viewsync_crop.txt')
|
|
9
|
+
|
|
10
|
+
# ─── Color Palette ───
|
|
11
|
+
BG = "#1e1e2e"
|
|
12
|
+
SURFACE = "#2a2a3d"
|
|
13
|
+
CARD = "#313146"
|
|
14
|
+
ACCENT = "#7c3aed"
|
|
15
|
+
ACCENT_HVR = "#9b59f0"
|
|
16
|
+
GREEN = "#22c55e"
|
|
17
|
+
GREEN_HVR = "#16a34a"
|
|
18
|
+
BLUE = "#3b82f6"
|
|
19
|
+
BLUE_HVR = "#2563eb"
|
|
20
|
+
RED = "#ef4444"
|
|
21
|
+
RED_HVR = "#dc2626"
|
|
22
|
+
TEXT = "#e2e8f0"
|
|
23
|
+
TEXT_DIM = "#94a3b8"
|
|
24
|
+
SLIDER_BG = "#3f3f5a"
|
|
25
|
+
|
|
26
|
+
def styled_btn(parent, text, bg_color, hover_color, command, font_size=11, pady=8):
|
|
27
|
+
b = tk.Button(parent, text=text, bg=bg_color, fg="white", activebackground=hover_color,
|
|
28
|
+
activeforeground="white", font=("Segoe UI", font_size, "bold"),
|
|
29
|
+
relief="flat", bd=0, cursor="hand2", command=command, pady=pady)
|
|
30
|
+
b.bind("<Enter>", lambda e: b.config(bg=hover_color))
|
|
31
|
+
b.bind("<Leave>", lambda e: b.config(bg=bg_color))
|
|
32
|
+
return b
|
|
33
|
+
|
|
34
|
+
class ScrcpyApp:
|
|
35
|
+
def __init__(self, root):
|
|
36
|
+
self.root = root
|
|
37
|
+
self.root.title("ViewSync Master v1.0.0")
|
|
38
|
+
self.root.geometry("460x680")
|
|
39
|
+
self.root.configure(bg=BG)
|
|
40
|
+
self.root.minsize(400, 500)
|
|
41
|
+
|
|
42
|
+
self.process = None
|
|
43
|
+
self.profiles = {}
|
|
44
|
+
self.current_profile = tk.StringVar()
|
|
45
|
+
self.val_l = tk.IntVar(value=0)
|
|
46
|
+
self.val_r = tk.IntVar(value=0)
|
|
47
|
+
self.val_t = tk.IntVar(value=0)
|
|
48
|
+
self.val_b = tk.IntVar(value=0)
|
|
49
|
+
self._status_timer = None
|
|
50
|
+
|
|
51
|
+
self.load_profiles()
|
|
52
|
+
self.build_ui()
|
|
53
|
+
self.on_profile_select(None)
|
|
54
|
+
|
|
55
|
+
def load_profiles(self):
|
|
56
|
+
if os.path.exists(CONFIG_FILE):
|
|
57
|
+
try:
|
|
58
|
+
with open(CONFIG_FILE, 'r') as f:
|
|
59
|
+
self.profiles = json.load(f)
|
|
60
|
+
except:
|
|
61
|
+
pass
|
|
62
|
+
if not self.profiles:
|
|
63
|
+
self.profiles = {"Physics Wallah": {"L": 0, "R": 0, "T": 0, "B": 0}}
|
|
64
|
+
if os.path.exists(OLD_CONFIG):
|
|
65
|
+
try:
|
|
66
|
+
with open(OLD_CONFIG, 'r') as f:
|
|
67
|
+
l, r, t, b = map(int, f.read().strip().split(','))
|
|
68
|
+
self.profiles["Physics Wallah"] = {"L": l, "R": r, "T": t, "B": b}
|
|
69
|
+
except:
|
|
70
|
+
pass
|
|
71
|
+
self.save_profiles()
|
|
72
|
+
self.current_profile.set(list(self.profiles.keys())[0])
|
|
73
|
+
|
|
74
|
+
def save_profiles(self):
|
|
75
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
76
|
+
json.dump(self.profiles, f, indent=4)
|
|
77
|
+
|
|
78
|
+
def show_status(self, msg, color=GREEN):
|
|
79
|
+
self.status_lbl.config(text=msg, fg=color, bg="#1a3a1a" if color == GREEN else "#3a1a1a" if color == RED else SURFACE)
|
|
80
|
+
if self._status_timer:
|
|
81
|
+
self.root.after_cancel(self._status_timer)
|
|
82
|
+
self._status_timer = self.root.after(3000, lambda: self.status_lbl.config(text="Ready", fg=TEXT_DIM, bg=SURFACE))
|
|
83
|
+
|
|
84
|
+
def build_ui(self):
|
|
85
|
+
title_bar = tk.Frame(self.root, bg=ACCENT, height=50)
|
|
86
|
+
title_bar.pack(fill=tk.X)
|
|
87
|
+
title_bar.pack_propagate(False)
|
|
88
|
+
tk.Label(title_bar, text="⚡ ViewSync Master v1.0.0", bg=ACCENT, fg="white", font=("Segoe UI", 14, "bold")).pack(side=tk.LEFT, padx=15, pady=10)
|
|
89
|
+
|
|
90
|
+
container = tk.Frame(self.root, bg=BG)
|
|
91
|
+
container.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
|
|
92
|
+
|
|
93
|
+
self._card(container, "📁 PROFILES", self._build_profile_card)
|
|
94
|
+
self._card(container, "✂️ CROP SETTINGS", self._build_crop_card)
|
|
95
|
+
self._card(container, "🚀 LAUNCH", self._build_launch_card)
|
|
96
|
+
|
|
97
|
+
status_frame = tk.Frame(self.root, bg=SURFACE, height=32)
|
|
98
|
+
status_frame.pack(fill=tk.X, side=tk.BOTTOM)
|
|
99
|
+
status_frame.pack_propagate(False)
|
|
100
|
+
self.status_lbl = tk.Label(status_frame, text="Ready", bg=SURFACE, fg=TEXT_DIM, font=("Segoe UI", 10), anchor="w", padx=15)
|
|
101
|
+
self.status_lbl.pack(fill=tk.BOTH, expand=True)
|
|
102
|
+
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
|
|
103
|
+
|
|
104
|
+
def _card(self, parent, title, builder):
|
|
105
|
+
outer = tk.Frame(parent, bg=CARD, highlightbackground="#404060", highlightthickness=1)
|
|
106
|
+
outer.pack(fill=tk.X, pady=6)
|
|
107
|
+
header = tk.Frame(outer, bg=CARD)
|
|
108
|
+
header.pack(fill=tk.X, padx=12, pady=(10, 0))
|
|
109
|
+
tk.Label(header, text=title, bg=CARD, fg=TEXT_DIM, font=("Segoe UI", 9, "bold")).pack(anchor=tk.W)
|
|
110
|
+
body = tk.Frame(outer, bg=CARD)
|
|
111
|
+
body.pack(fill=tk.X, padx=12, pady=(5, 12))
|
|
112
|
+
builder(body)
|
|
113
|
+
|
|
114
|
+
def _build_profile_card(self, parent):
|
|
115
|
+
row1 = tk.Frame(parent, bg=CARD)
|
|
116
|
+
row1.pack(fill=tk.X, pady=(0, 8))
|
|
117
|
+
tk.Label(row1, text="Active:", bg=CARD, fg=TEXT, font=("Segoe UI", 10)).pack(side=tk.LEFT)
|
|
118
|
+
self.profile_cb = ttk.Combobox(row1, textvariable=self.current_profile, state="readonly", width=28, font=("Segoe UI", 10))
|
|
119
|
+
self.profile_cb['values'] = list(self.profiles.keys())
|
|
120
|
+
self.profile_cb.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True)
|
|
121
|
+
self.profile_cb.bind('<<ComboboxSelected>>', self.on_profile_select)
|
|
122
|
+
|
|
123
|
+
row2 = tk.Frame(parent, bg=CARD)
|
|
124
|
+
row2.pack(fill=tk.X)
|
|
125
|
+
for txt, cmd in [("➕ New", self.new_profile), ("💾 Save", self.save_current_profile),
|
|
126
|
+
("✏️ Rename", self.rename_profile), ("🗑️ Delete", self.delete_profile)]:
|
|
127
|
+
b = tk.Button(row2, text=txt, bg=SURFACE, fg=TEXT, font=("Segoe UI", 9, "bold"),
|
|
128
|
+
relief="flat", padx=8, pady=5, cursor="hand2", command=cmd,
|
|
129
|
+
activebackground=ACCENT, activeforeground="white", bd=0)
|
|
130
|
+
b.pack(side=tk.LEFT, padx=2, expand=True, fill=tk.X)
|
|
131
|
+
|
|
132
|
+
def _build_crop_card(self, parent):
|
|
133
|
+
for label, var in [("Left", self.val_l), ("Right", self.val_r),
|
|
134
|
+
("Top", self.val_t), ("Bottom", self.val_b)]:
|
|
135
|
+
self._slider_row(parent, label, var)
|
|
136
|
+
|
|
137
|
+
def _slider_row(self, parent, label, var):
|
|
138
|
+
row = tk.Frame(parent, bg=CARD)
|
|
139
|
+
row.pack(fill=tk.X, pady=3)
|
|
140
|
+
tk.Label(row, text=label, bg=CARD, fg=TEXT, width=7, anchor="w", font=("Segoe UI", 10, "bold")).pack(side=tk.LEFT)
|
|
141
|
+
scale = tk.Scale(row, from_=0, to=500, orient=tk.HORIZONTAL, variable=var,
|
|
142
|
+
bg=CARD, fg=TEXT, troughcolor=SLIDER_BG, highlightthickness=0,
|
|
143
|
+
activebackground=ACCENT, sliderrelief="flat", showvalue=False, sliderlength=18, bd=0)
|
|
144
|
+
scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 10))
|
|
145
|
+
val_lbl = tk.Label(row, text="0", bg=SURFACE, fg=ACCENT, width=4, font=("Segoe UI", 10, "bold"), padx=4, pady=2)
|
|
146
|
+
val_lbl.pack(side=tk.RIGHT)
|
|
147
|
+
def update_lbl(*args):
|
|
148
|
+
val_lbl.config(text=str(var.get()))
|
|
149
|
+
var.trace_add("write", update_lbl)
|
|
150
|
+
update_lbl()
|
|
151
|
+
|
|
152
|
+
def _build_launch_card(self, parent):
|
|
153
|
+
b1 = styled_btn(parent, "▶ Normal Phone (Bina Crop Ke)", ACCENT, ACCENT_HVR, self.launch_normal, font_size=12, pady=10)
|
|
154
|
+
b1.pack(fill=tk.X, pady=(0, 8))
|
|
155
|
+
row = tk.Frame(parent, bg=CARD)
|
|
156
|
+
row.pack(fill=tk.X, pady=(0, 8))
|
|
157
|
+
b2 = styled_btn(row, "🖥 Cropped Fullscreen", GREEN, GREEN_HVR, lambda: self.launch_cropped(True), font_size=10, pady=8)
|
|
158
|
+
b2.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 4))
|
|
159
|
+
b3 = styled_btn(row, "🔍 Preview Window", BLUE, BLUE_HVR, lambda: self.launch_cropped(False), font_size=10, pady=8)
|
|
160
|
+
b3.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(4, 0))
|
|
161
|
+
b4 = styled_btn(parent, "■ Stop ViewSync", RED, RED_HVR, self.stop_viewsync, font_size=10, pady=8)
|
|
162
|
+
b4.pack(fill=tk.X)
|
|
163
|
+
|
|
164
|
+
def on_profile_select(self, event):
|
|
165
|
+
p = self.current_profile.get()
|
|
166
|
+
if p in self.profiles:
|
|
167
|
+
v = self.profiles[p]
|
|
168
|
+
self.val_l.set(v.get("L", 0))
|
|
169
|
+
self.val_r.set(v.get("R", 0))
|
|
170
|
+
self.val_t.set(v.get("T", 0))
|
|
171
|
+
self.val_b.set(v.get("B", 0))
|
|
172
|
+
|
|
173
|
+
def _refresh_cb(self):
|
|
174
|
+
self.profile_cb['values'] = list(self.profiles.keys())
|
|
175
|
+
|
|
176
|
+
def new_profile(self):
|
|
177
|
+
name = simpledialog.askstring("New Profile", "Profile ka naam daalein:")
|
|
178
|
+
if name and name.strip():
|
|
179
|
+
name = name.strip()
|
|
180
|
+
if name in self.profiles:
|
|
181
|
+
self.show_status(f"❌ '{name}' pehle se exist karta hai!", RED)
|
|
182
|
+
return
|
|
183
|
+
self.profiles[name] = {"L": self.val_l.get(), "R": self.val_r.get(), "T": self.val_t.get(), "B": self.val_b.get()}
|
|
184
|
+
self.save_profiles()
|
|
185
|
+
self.current_profile.set(name)
|
|
186
|
+
self._refresh_cb()
|
|
187
|
+
self.show_status(f"✅ Profile '{name}' created!")
|
|
188
|
+
|
|
189
|
+
def save_current_profile(self):
|
|
190
|
+
p = self.current_profile.get()
|
|
191
|
+
self.profiles[p] = {"L": self.val_l.get(), "R": self.val_r.get(), "T": self.val_t.get(), "B": self.val_b.get()}
|
|
192
|
+
self.save_profiles()
|
|
193
|
+
self.show_status(f"✅ Profile '{p}' saved! (L:{self.val_l.get()} R:{self.val_r.get()} T:{self.val_t.get()} B:{self.val_b.get()})")
|
|
194
|
+
|
|
195
|
+
def rename_profile(self):
|
|
196
|
+
old = self.current_profile.get()
|
|
197
|
+
new = simpledialog.askstring("Rename", f"'{old}' ka naya naam:")
|
|
198
|
+
if new and new.strip() and new.strip() != old:
|
|
199
|
+
new = new.strip()
|
|
200
|
+
if new in self.profiles:
|
|
201
|
+
self.show_status(f"❌ '{new}' pehle se hai!", RED)
|
|
202
|
+
return
|
|
203
|
+
self.profiles[new] = self.profiles.pop(old)
|
|
204
|
+
self.save_profiles()
|
|
205
|
+
self.current_profile.set(new)
|
|
206
|
+
self._refresh_cb()
|
|
207
|
+
self.show_status(f"✅ Renamed to '{new}'")
|
|
208
|
+
|
|
209
|
+
def delete_profile(self):
|
|
210
|
+
p = self.current_profile.get()
|
|
211
|
+
if len(self.profiles) <= 1:
|
|
212
|
+
self.show_status("❌ Aakhri profile delete nahi kar sakte!", RED)
|
|
213
|
+
return
|
|
214
|
+
if messagebox.askyesno("Delete?", f"Kya '{p}' delete karein?"):
|
|
215
|
+
del self.profiles[p]
|
|
216
|
+
self.save_profiles()
|
|
217
|
+
self.current_profile.set(list(self.profiles.keys())[0])
|
|
218
|
+
self._refresh_cb()
|
|
219
|
+
self.on_profile_select(None)
|
|
220
|
+
self.show_status(f"🗑️ Profile '{p}' deleted")
|
|
221
|
+
|
|
222
|
+
def _start_helpers(self):
|
|
223
|
+
subprocess.Popen("if ! pgrep -f viewsync_helper.py > /dev/null; then nohup python3 ~/.viewsync-master/viewsync_helper.py > /dev/null 2>&1 & fi", shell=True)
|
|
224
|
+
|
|
225
|
+
def launch_normal(self):
|
|
226
|
+
self.stop_viewsync()
|
|
227
|
+
self._start_helpers()
|
|
228
|
+
# Added -b 4M --max-fps 30 for smooth playback
|
|
229
|
+
cmd = ["/usr/local/bin/scrcpy", "--window-title=viewsync", "-m", "1024", "-b", "2M", "--max-fps", "30"]
|
|
230
|
+
env = os.environ.copy()
|
|
231
|
+
env["ADB"] = "/usr/bin/adb"
|
|
232
|
+
self.process = subprocess.Popen(cmd, env=env)
|
|
233
|
+
self.show_status("▶ ViewSync launched (Normal Mode - Smooth)")
|
|
234
|
+
|
|
235
|
+
def launch_cropped(self, fullscreen):
|
|
236
|
+
self.stop_viewsync()
|
|
237
|
+
self._start_helpers()
|
|
238
|
+
p = self.current_profile.get()
|
|
239
|
+
self.profiles[p] = {"L": self.val_l.get(), "R": self.val_r.get(), "T": self.val_t.get(), "B": self.val_b.get()}
|
|
240
|
+
self.save_profiles()
|
|
241
|
+
|
|
242
|
+
v = self.profiles[p]
|
|
243
|
+
L, R, T, B = v["L"], v["R"], v["T"], v["B"]
|
|
244
|
+
PW = 1080 - T - B
|
|
245
|
+
PH = 2280 - L - R
|
|
246
|
+
crop = f"{PW}:{PH}:{B}:{L}"
|
|
247
|
+
|
|
248
|
+
# Added -b 4M --max-fps 30 for smooth playback
|
|
249
|
+
cmd = ["/usr/local/bin/scrcpy", "--window-title=viewsync", "--crop", crop, "-m", "1024", "-b", "2M", "--max-fps", "30"]
|
|
250
|
+
if fullscreen:
|
|
251
|
+
cmd.append("-f")
|
|
252
|
+
env = os.environ.copy()
|
|
253
|
+
env["ADB"] = "/usr/bin/adb"
|
|
254
|
+
self.process = subprocess.Popen(cmd, env=env)
|
|
255
|
+
mode = "Fullscreen" if fullscreen else "Windowed"
|
|
256
|
+
self.show_status(f"▶ ViewSync launched ({mode} - Smooth) — Profile: {p}")
|
|
257
|
+
|
|
258
|
+
def stop_viewsync(self):
|
|
259
|
+
if self.process:
|
|
260
|
+
try:
|
|
261
|
+
self.process.terminate()
|
|
262
|
+
self.process.wait(timeout=2)
|
|
263
|
+
except:
|
|
264
|
+
self.process.kill()
|
|
265
|
+
self.process = None
|
|
266
|
+
subprocess.run(["pkill", "-f", "viewsync_helper.py"]); import time; time.sleep(0.5)
|
|
267
|
+
|
|
268
|
+
def on_close(self):
|
|
269
|
+
self.stop_viewsync()
|
|
270
|
+
self.root.destroy()
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
root = tk.Tk()
|
|
274
|
+
app = ScrcpyApp(root)
|
|
275
|
+
root.mainloop()
|