readme
This commit is contained in:
parent
9a1171bb35
commit
140b57aea1
77
README.md
Normal file
77
README.md
Normal file
@ -0,0 +1,77 @@
|
||||
# 🚀 Bubblecam - Your Webcam, Your Way!
|
||||
|
||||
## 🎥 What's Bubblecam?
|
||||
Bubblecam lets you control your screen recoding like a boss. Use your **real camera** or switch to a **custom avatar**, drag it anywhere on your screen, and even **record your screen**! Works like magic in Microsoft Edge. 😎
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Key Features
|
||||
✅ **Toggle Between Camera & Avatar** - Be on cam or flex your avatar, your choice!
|
||||
✅ **Custom Avatar Upload** - Upload your own avatar to replace your webcam feed.
|
||||
✅ **Draggable Camera Bubble** - Move it **anywhere** on the screen, no restrictions!
|
||||
✅ **Screen Recording** - Capture your screen directly from the pop-up.
|
||||
✅ **Easy to Use** - No complicated setup, just load & go!
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
- **HTML** (for the UI)
|
||||
- **JavaScript** (for all the cool interactivity)
|
||||
|
||||
---
|
||||
|
||||
## 📂 Project Structure
|
||||
```
|
||||
bubblecam/
|
||||
│
|
||||
├── icons/ # All the cool icons
|
||||
│
|
||||
├── camera.html # Main camera interface
|
||||
├── camera.js # Handles camera logic
|
||||
├── content.js # Manages draggable camera bubble
|
||||
├── desktopRecord.html # UI for screen recording
|
||||
├── desktopRecord.js # Logic for screen recording
|
||||
├── offscreen.html # Off-screen camera handling
|
||||
├── offscreen.js # Off-screen magic happens here
|
||||
├── popup.html # Pop-up UI
|
||||
├── popup.js # Controls pop-up window
|
||||
├── service-worker.js # Injects & removes camera based on activity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ How to Install & Use in Microsoft Edge (100% Free!)
|
||||
### **1️⃣ Download & Extract the ZIP**
|
||||
- Hit **Download ZIP** (if it's from GitHub, click the green `<> Code` button > `Download ZIP`).
|
||||
- Extract the folder to your PC.
|
||||
|
||||
### **2️⃣ Load It as an Unpacked Extension in Edge**
|
||||
1. Open **Microsoft Edge** (yep, only Edge, sorry Chrome gang 🫠).
|
||||
2. Go to `edge://extensions/`.
|
||||
3. Toggle **Developer Mode** (top-right corner).
|
||||
4. Click **"Load unpacked"**.
|
||||
5. Select the **bubblecam** folder (the one you extracted).
|
||||
6. Boom! Bubblecam is installed. 🎉
|
||||
|
||||
### **3️⃣ How to Use Bubblecam**
|
||||
1. Click on the **Bubblecam icon** in Edge’s toolbar.
|
||||
2. Choose **Camera Mode** or **Avatar Mode**.
|
||||
3. **Upload Your Avatar** (if using avatar mode).
|
||||
4. Drag your camera bubble anywhere you want! 🏀
|
||||
5. Click **Record** to start screen recording.
|
||||
6. Have fun being the director of your own screen. 🎬
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Prerequisites
|
||||
- A PC with **Microsoft Edge** installed.
|
||||
- Camera & microphone permissions enabled (duh, you need a cam!).
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
Licensed under the **MIT License** – basically, you're free to use it however you want! Just don’t sue me if your avatar starts vibing too hard. 😆
|
||||
|
||||
---
|
||||
|
||||
|
74
camera.html
74
camera.html
@ -1,29 +1,59 @@
|
||||
<!-- //camera.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: rgb(168, 29, 203);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div#camera {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Camera</title>
|
||||
<style>
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#camera {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#cam {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.avatar-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #5d3fbd;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="camera"></div>
|
||||
<script src="camera.js"></script>
|
||||
<div id="camera"></div>
|
||||
<script src="camera.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
117
camera.js
117
camera.js
@ -6,60 +6,66 @@ const runCode = async () => {
|
||||
const cameraElement = document.querySelector("#camera");
|
||||
|
||||
const startCamera = async () => {
|
||||
// Check permissions first
|
||||
const permissions = await navigator.permissions.query({
|
||||
name: "camera",
|
||||
});
|
||||
|
||||
if (permissions.state === "prompt") {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (permissions.state === "denied") {
|
||||
alert("Camera permissions denied");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only proceed with camera if we're in camera mode
|
||||
if (currentMode === 'avatar') {
|
||||
displayAvatar();
|
||||
return;
|
||||
}
|
||||
|
||||
const videoElement = document.createElement("video");
|
||||
videoElement.setAttribute("id", "cam");
|
||||
videoElement.setAttribute(
|
||||
"style",
|
||||
`
|
||||
height: 200px;
|
||||
border-radius: 100px;
|
||||
transform: scaleX(-1);
|
||||
`
|
||||
);
|
||||
videoElement.setAttribute("autoplay", true);
|
||||
videoElement.setAttribute("muted", true);
|
||||
|
||||
try {
|
||||
const permissions = await navigator.permissions.query({
|
||||
name: "camera",
|
||||
});
|
||||
|
||||
if (permissions.state === "prompt") {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (permissions.state === "denied") {
|
||||
console.log("Camera permissions denied, falling back to avatar");
|
||||
currentMode = 'avatar';
|
||||
displayAvatar();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (currentMode === 'avatar') {
|
||||
displayAvatar();
|
||||
return;
|
||||
}
|
||||
|
||||
const videoElement = document.createElement("video");
|
||||
videoElement.setAttribute("id", "cam");
|
||||
videoElement.setAttribute(
|
||||
"style",
|
||||
`
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
border-radius: 100px;
|
||||
transform: scaleX(-1);
|
||||
object-fit: cover;
|
||||
`
|
||||
);
|
||||
videoElement.setAttribute("autoplay", true);
|
||||
videoElement.setAttribute("muted", true);
|
||||
|
||||
cameraStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: true,
|
||||
video: {
|
||||
width: { ideal: 400 },
|
||||
height: { ideal: 400 },
|
||||
facingMode: "user"
|
||||
}
|
||||
});
|
||||
|
||||
videoElement.srcObject = cameraStream;
|
||||
|
||||
// Clear any existing content
|
||||
cameraElement.innerHTML = '';
|
||||
cameraElement.appendChild(videoElement);
|
||||
} catch (err) {
|
||||
console.error("Error accessing camera:", err);
|
||||
// Fallback to avatar if camera fails
|
||||
currentMode = 'avatar';
|
||||
displayAvatar();
|
||||
}
|
||||
};
|
||||
|
||||
const displayAvatar = () => {
|
||||
// Stop any existing camera stream
|
||||
if (cameraStream) {
|
||||
stopCamera();
|
||||
}
|
||||
@ -73,15 +79,42 @@ const runCode = async () => {
|
||||
width: 200px;
|
||||
border-radius: 100px;
|
||||
object-fit: cover;
|
||||
background-color: #5d3fbd;
|
||||
`
|
||||
);
|
||||
avatarImg.src = avatarData || 'default-avatar.png';
|
||||
|
||||
// Clear any existing content
|
||||
|
||||
if (avatarData) {
|
||||
avatarImg.src = avatarData;
|
||||
} else {
|
||||
|
||||
avatarImg.src = 'avatar.png';
|
||||
avatarImg.onerror = () => {
|
||||
|
||||
avatarImg.style.backgroundColor = '#5d3fbd';
|
||||
avatarImg.style.display = 'flex';
|
||||
avatarImg.style.justifyContent = 'center';
|
||||
avatarImg.style.alignItems = 'center';
|
||||
avatarImg.src = '';
|
||||
|
||||
// Create a text element for initials
|
||||
const initialsElem = document.createElement('div');
|
||||
initialsElem.textContent = 'BC';
|
||||
initialsElem.style.color = 'white';
|
||||
initialsElem.style.fontSize = '60px';
|
||||
initialsElem.style.fontWeight = 'bold';
|
||||
|
||||
cameraElement.innerHTML = '';
|
||||
cameraElement.appendChild(initialsElem);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
cameraElement.innerHTML = '';
|
||||
cameraElement.appendChild(avatarImg);
|
||||
};
|
||||
|
||||
|
||||
const stopCamera = () => {
|
||||
if (cameraStream) {
|
||||
cameraStream.getTracks().forEach(track => {
|
||||
@ -101,7 +134,6 @@ const runCode = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Message listeners for camera/avatar control
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === "stop-camera") {
|
||||
stopCamera();
|
||||
@ -125,7 +157,7 @@ const runCode = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize based on saved mode
|
||||
|
||||
chrome.storage.local.get(['mode', 'avatarData'], function(data) {
|
||||
currentMode = data.mode || 'camera';
|
||||
avatarData = data.avatarData;
|
||||
@ -138,4 +170,5 @@ const runCode = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
runCode();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', runCode);
|
335
offscreen.js
335
offscreen.js
@ -1,182 +1,173 @@
|
||||
//offscreen.js
|
||||
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
|
||||
console.log("[offscreen] message received", message, sender);
|
||||
|
||||
switch (message.type) {
|
||||
case "start-recording":
|
||||
console.log("start recording received in offscreen.js");
|
||||
|
||||
await startRecording(message.data, message.quality);
|
||||
sendResponse({ status: "recording-started" });
|
||||
console.log("[offscreen] message received", message, sender);
|
||||
|
||||
switch (message.type) {
|
||||
case "start-recording":
|
||||
console.log("start recording received in offscreen.js");
|
||||
|
||||
await startRecording(message.data, message.quality);
|
||||
sendResponse({ status: "recording-started" });
|
||||
break;
|
||||
case "stop-recording":
|
||||
console.log("stop recording received in offscreen.js");
|
||||
|
||||
await stopRecording();
|
||||
sendResponse({ status: "recording-stopped" });
|
||||
break;
|
||||
default:
|
||||
console.log("default");
|
||||
sendResponse({ status: "unknown-message" });
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
let recorder;
|
||||
let data = [];
|
||||
|
||||
async function stopRecording() {
|
||||
console.log("Entered stopRecording");
|
||||
|
||||
if (recorder?.state === "recording") {
|
||||
console.log("Recorder state is 'recording', stopping...");
|
||||
recorder.stop();
|
||||
|
||||
// Send a message to the content script to stop the camera
|
||||
chrome.runtime.sendMessage({ type: "stop-camera" }, response => {
|
||||
if (response?.status === "camera-stopped") {
|
||||
console.log("Camera has been successfully stopped.");
|
||||
} else {
|
||||
console.log("Failed to stop the camera.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("No active recording found or recorder is not in 'recording' state.");
|
||||
}
|
||||
|
||||
console.log("Stopped the recording");
|
||||
}
|
||||
|
||||
function stopAllMediaStreams(media, microphone) {
|
||||
media.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
console.log("Media Track stopped:", track);
|
||||
});
|
||||
|
||||
microphone.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
console.log("Microphone Track stopped", track);
|
||||
});
|
||||
}
|
||||
|
||||
async function startRecording(streamId, quality) {
|
||||
try {
|
||||
if (recorder?.state === "recording") {
|
||||
throw new Error("Called startRecording while recording is in progress.");
|
||||
}
|
||||
|
||||
console.log("start recording", streamId);
|
||||
console.log("qaulity inside offfscreen.js", quality);
|
||||
|
||||
let videoConstraints;
|
||||
|
||||
switch (quality) {
|
||||
case "low":
|
||||
videoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
maxWidth: 640,
|
||||
maxHeight: 480,
|
||||
minWidth: 640,
|
||||
minHeight: 480,
|
||||
maxFrameRate: 15,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case "stop-recording":
|
||||
console.log("stop recording received in offscreen.js");
|
||||
|
||||
await stopRecording();
|
||||
sendResponse({ status: "recording-stopped" });
|
||||
case "medium":
|
||||
videoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720,
|
||||
minWidth: 1280,
|
||||
minHeight: 720,
|
||||
maxFrameRate: 30,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case "high":
|
||||
videoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
minWidth: 1920,
|
||||
minHeight: 1080,
|
||||
maxFrameRate: 60,
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.log("default");
|
||||
sendResponse({ status: "unknown-message" });
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
let recorder;
|
||||
let data = [];
|
||||
|
||||
async function stopRecording() {
|
||||
console.log("Entered stopRecording");
|
||||
|
||||
if (recorder?.state === "recording") {
|
||||
console.log("Recorder state is 'recording', stopping...");
|
||||
recorder.stop();
|
||||
|
||||
// Send a message to the content script to stop the camera
|
||||
chrome.runtime.sendMessage({ type: "stop-camera" }, response => {
|
||||
if (response?.status === "camera-stopped") {
|
||||
console.log("Camera has been successfully stopped.");
|
||||
} else {
|
||||
console.log("Failed to stop the camera.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("No active recording found or recorder is not in 'recording' state.");
|
||||
}
|
||||
|
||||
console.log("Stopped the recording");
|
||||
}
|
||||
|
||||
function stopAllMediaStreams(media, microphone) {
|
||||
media.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
console.log("Media Track stopped:", track);
|
||||
});
|
||||
|
||||
microphone.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
console.log("Microphone Track stopped", track);
|
||||
});
|
||||
}
|
||||
|
||||
async function startRecording(streamId, quality) {
|
||||
try {
|
||||
if (recorder?.state === "recording") {
|
||||
throw new Error("Called startRecording while recording is in progress.");
|
||||
}
|
||||
|
||||
console.log("start recording", streamId);
|
||||
console.log("qaulity inside offfscreen.js", quality);
|
||||
|
||||
|
||||
let videoConstraints;
|
||||
|
||||
switch (quality) {
|
||||
case "low":
|
||||
videoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
maxWidth: 640,
|
||||
maxHeight: 480,
|
||||
minWidth: 640,
|
||||
minHeight: 480,
|
||||
maxFrameRate: 15,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case "medium":
|
||||
videoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720,
|
||||
minWidth: 1280,
|
||||
minHeight: 720,
|
||||
maxFrameRate: 30,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case "high":
|
||||
videoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
minWidth: 1920,
|
||||
minHeight: 1080,
|
||||
maxFrameRate: 60,
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
videoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
const media = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
videoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
},
|
||||
},
|
||||
video: videoConstraints
|
||||
});
|
||||
|
||||
|
||||
const microphone = await navigator.mediaDevices.getUserMedia({
|
||||
audio: { echoCancellation: false },
|
||||
});
|
||||
|
||||
const mixedContext = new AudioContext();
|
||||
const mixedDest = mixedContext.createMediaStreamDestination();
|
||||
|
||||
mixedContext.createMediaStreamSource(microphone).connect(mixedDest);
|
||||
mixedContext.createMediaStreamSource(media).connect(mixedDest);
|
||||
|
||||
const combinedStream = new MediaStream([
|
||||
media.getVideoTracks()[0],
|
||||
mixedDest.stream.getTracks()[0],
|
||||
]);
|
||||
|
||||
recorder = new MediaRecorder(combinedStream, { mimeType: "video/webm" });
|
||||
|
||||
|
||||
recorder.ondataavailable = (event) => {
|
||||
console.log("data available", event);
|
||||
data.push(event.data);
|
||||
};
|
||||
|
||||
|
||||
recorder.onstop = async () => {
|
||||
console.log("recording stopped");
|
||||
// send the data to the service worker
|
||||
console.log("sending data to service worker");
|
||||
stopAllMediaStreams(media, microphone);
|
||||
|
||||
recorder = null;
|
||||
|
||||
|
||||
const blob = new Blob(data, { type: "video/webm" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
chrome.runtime.sendMessage({ type: "open-tab", url });
|
||||
};
|
||||
|
||||
|
||||
recorder.start();
|
||||
} catch (err) {
|
||||
console.log("error", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const media = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
mandatory: {
|
||||
chromeMediaSource: "tab",
|
||||
chromeMediaSourceId: streamId,
|
||||
},
|
||||
},
|
||||
video: videoConstraints
|
||||
});
|
||||
|
||||
const microphone = await navigator.mediaDevices.getUserMedia({
|
||||
audio: { echoCancellation: false },
|
||||
});
|
||||
|
||||
const mixedContext = new AudioContext();
|
||||
const mixedDest = mixedContext.createMediaStreamDestination();
|
||||
|
||||
mixedContext.createMediaStreamSource(microphone).connect(mixedDest);
|
||||
mixedContext.createMediaStreamSource(media).connect(mixedDest);
|
||||
|
||||
const combinedStream = new MediaStream([
|
||||
media.getVideoTracks()[0],
|
||||
mixedDest.stream.getTracks()[0],
|
||||
]);
|
||||
|
||||
recorder = new MediaRecorder(combinedStream, { mimeType: "video/webm" });
|
||||
|
||||
recorder.ondataavailable = (event) => {
|
||||
console.log("data available", event);
|
||||
data.push(event.data);
|
||||
};
|
||||
|
||||
recorder.onstop = async () => {
|
||||
console.log("recording stopped");
|
||||
// send the data to the service worker
|
||||
console.log("sending data to service worker");
|
||||
stopAllMediaStreams(media, microphone);
|
||||
|
||||
recorder = null;
|
||||
|
||||
const blob = new Blob(data, { type: "video/webm" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
chrome.runtime.sendMessage({ type: "open-tab", url });
|
||||
};
|
||||
|
||||
recorder.start();
|
||||
} catch (err) {
|
||||
console.log("error", err);
|
||||
}
|
||||
}
|
133
popup.html
133
popup.html
@ -16,7 +16,7 @@
|
||||
<style>
|
||||
body {
|
||||
width: 300px;
|
||||
height: 320px;
|
||||
height: 340px;
|
||||
font-family: Poppins, sans-serif;
|
||||
background: #251f38;
|
||||
--gap: 5em;
|
||||
@ -54,22 +54,27 @@
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mode-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.mode-button.active {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.avatar-upload {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
@ -81,6 +86,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.avatar-preview:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.avatar-preview img {
|
||||
@ -97,17 +109,85 @@
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.record-options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 25px;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
|
||||
.record-btn {
|
||||
border: none;
|
||||
color: white;
|
||||
text-align: center;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 15px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.record-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.record-btn p {
|
||||
margin: 5px 0 0 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.quality-select {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border: 1.5px solid rgb(255, 255, 255);
|
||||
font-family: Poppins;
|
||||
padding: 8px 15px;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.quality-select option {
|
||||
color: black;
|
||||
font-size: 0.75rem;
|
||||
padding: 8px 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h1 style="font-weight: 400; margin-bottom:20px;"> Recorder </h1>
|
||||
<h1 style="font-weight: 500; margin-bottom:20px; text-align: center;">BUBBLE CAM</h1>
|
||||
|
||||
<!-- Camera/Avatar Mode Selector -->
|
||||
<div class="mode-selector">
|
||||
@ -124,44 +204,51 @@
|
||||
<!-- Avatar Upload Section -->
|
||||
<div id="avatar-section" class="avatar-upload">
|
||||
<div class="avatar-preview">
|
||||
<img id="avatar-img" src="default-avatar.png" alt="Avatar">
|
||||
<img id="avatar-img" src="avatar.png" alt="Avatar">
|
||||
</div>
|
||||
<input type="file" id="avatar-input" accept="image/*" style="display: none;">
|
||||
<button class="upload-btn" onclick="document.getElementById('avatar-input').click()">
|
||||
<!-- Make the file input directly visible -->
|
||||
<label for="avatar-input" class="upload-btn">
|
||||
<i class="fas fa-upload"></i>
|
||||
Upload Avatar
|
||||
</button>
|
||||
</label>
|
||||
<input type="file" id="avatar-input" accept="image/*" style="position: absolute; opacity: 0; width: 0; height: 0;">
|
||||
</div>
|
||||
|
||||
<!-- Recording Options -->
|
||||
<div style="display: flex; flex-direction: row; gap: 25px;">
|
||||
<div style="display: flex; color: white;">
|
||||
<button id="tab"
|
||||
style="border: none; gap:10px; color: white; text-align: center; border-radius: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; padding:8px 10px; background-color: transparent; cursor: pointer;">
|
||||
<i id="tab-icon" style="color: white;" class="fa-regular fa-window-maximize fa-2xl"></i>
|
||||
<div class="record-options">
|
||||
<div class="record-option">
|
||||
<button id="tab" class="record-btn">
|
||||
<div class="icon-container">
|
||||
<i id="tab-icon" class="fa-regular fa-window-maximize fa-xl"></i>
|
||||
</div>
|
||||
<p>Window Tab</p>
|
||||
</button>
|
||||
</div>
|
||||
<div style="display: flex; color: white;">
|
||||
<button id="screen"
|
||||
style="border: none; color: white; gap:10px; text-align: center; border-radius: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; padding:8px 10px; background-color: transparent; cursor: pointer;">
|
||||
<i id="screen-icon" style="color: white;" class="fa-solid fa-display fa-2xl"></i>
|
||||
<div class="record-option">
|
||||
<button id="screen" class="record-btn">
|
||||
<div class="icon-container">
|
||||
<i id="screen-icon" class="fa-solid fa-display fa-xl"></i>
|
||||
</div>
|
||||
<p>Screen</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="options">
|
||||
<br>
|
||||
<select id="quality"
|
||||
style="background-color: transparent; color: white; border: 1.5px solid rgb(255, 255, 255);font-family: Poppins; padding: 3px; border-radius: 5px;"
|
||||
required>
|
||||
<option style="color: black; font-size:0.75rem; padding:8px 2px; cursor:pointer;" value="high" selected>
|
||||
High (1080p, 60fps)</option>
|
||||
<div id="options" style="display: flex; justify-content: center;">
|
||||
<select id="quality" class="quality-select" required>
|
||||
<option value="high" selected>High (1080p, 60fps)</option>
|
||||
<option value="medium">Medium (720p, 30fps)</option>
|
||||
<option value="low">Low (480p, 30fps)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
<script>
|
||||
document.getElementById('upload-avatar-btn').addEventListener('click', function() {
|
||||
document.getElementById('avatar-input').click();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
107
popup.js
107
popup.js
@ -1,12 +1,10 @@
|
||||
//popup.js
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const cameraMode = document.getElementById('camera-mode');
|
||||
const avatarMode = document.getElementById('avatar-mode');
|
||||
const avatarSection = document.getElementById('avatar-section');
|
||||
const avatarInput = document.getElementById('avatar-input');
|
||||
const avatarPreview = document.getElementById('avatar-img');
|
||||
|
||||
// Mode switching
|
||||
|
||||
cameraMode.addEventListener('click', () => {
|
||||
cameraMode.classList.add('active');
|
||||
avatarMode.classList.remove('active');
|
||||
@ -23,31 +21,86 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
chrome.runtime.sendMessage({ type: 'mode-change', mode: 'avatar' });
|
||||
});
|
||||
|
||||
// Avatar upload handling
|
||||
// Fix the avatar upload functionality
|
||||
avatarInput.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const avatarData = e.target.result;
|
||||
avatarPreview.src = avatarData;
|
||||
chrome.storage.local.set({ avatarData: avatarData });
|
||||
console.log("File input changed"); // Add this debug line
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
console.log("No file selected");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("File selected:", file.name); // Add this debug line
|
||||
|
||||
if (!file.type.match('image.*')) {
|
||||
alert('Please select an image file');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert('Image is too large. Please select an image under 2MB.');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const avatarData = e.target.result;
|
||||
console.log("File loaded successfully"); // Add this debug line
|
||||
|
||||
avatarPreview.src = avatarData;
|
||||
avatarPreview.style.display = 'block';
|
||||
|
||||
// Store the avatar data in Chrome storage
|
||||
chrome.storage.local.set({ avatarData: avatarData }, function() {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('Error saving avatar:', chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Avatar saved to storage"); // Add this debug line
|
||||
|
||||
// Notify any active content scripts about the avatar change
|
||||
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||
if (tabs[0]?.id) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'avatar-update',
|
||||
avatarData: avatarData
|
||||
type: 'avatar-update',
|
||||
avatarData: avatarData
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing avatar:', error);
|
||||
alert('There was an error processing your image. Please try another.');
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
console.error('Error reading file');
|
||||
alert('Error reading the image file. Please try again.');
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Load saved mode and avatar
|
||||
|
||||
|
||||
chrome.storage.local.get(['mode', 'avatarData'], function(data) {
|
||||
|
||||
if (data.mode === 'avatar') {
|
||||
avatarMode.click();
|
||||
if (data.avatarData) {
|
||||
avatarPreview.src = data.avatarData;
|
||||
}
|
||||
} else {
|
||||
cameraMode.click();
|
||||
}
|
||||
|
||||
if (data.avatarData) {
|
||||
avatarPreview.src = data.avatarData;
|
||||
avatarPreview.style.display = 'block';
|
||||
} else {
|
||||
|
||||
avatarPreview.src = 'avatar.png';
|
||||
avatarPreview.style.display = 'block';
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -68,11 +121,10 @@ const injectCamera = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize camera bubble when popup opens
|
||||
|
||||
injectCamera();
|
||||
|
||||
const removeCamera = async () => {
|
||||
|
||||
const tab = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!tab) return;
|
||||
|
||||
@ -80,18 +132,13 @@ const removeCamera = async () => {
|
||||
console.log("inject into tab", tabId);
|
||||
await chrome.scripting.executeScript({
|
||||
func: () => {
|
||||
const camera = document.querySelector("#purple-camera");
|
||||
if (!camera) return;
|
||||
camera.remove();
|
||||
document.querySelector("#purple-camera").style.display = "none";
|
||||
const camera = document.querySelector("#camera-wrapper");
|
||||
if (camera) camera.remove();
|
||||
},
|
||||
target: { tabId },
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const checkRecording = async () => {
|
||||
const recording = await chrome.storage.local.get(["recording", "type"]);
|
||||
const recordingStatus = recording.recording || false;
|
||||
@ -130,7 +177,6 @@ const init = async () => {
|
||||
const recordingState = await checkRecording();
|
||||
|
||||
if (recordingState[0] === true) {
|
||||
|
||||
chrome.runtime.sendMessage({ type: "stop-recording" });
|
||||
removeCamera();
|
||||
} else {
|
||||
@ -142,7 +188,6 @@ const init = async () => {
|
||||
injectCamera();
|
||||
}
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 100);
|
||||
@ -159,4 +204,4 @@ const init = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
init();
|
||||
init();
|
Loading…
x
Reference in New Issue
Block a user