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 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()
@@ -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()