-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcontent-script.js
More file actions
271 lines (239 loc) · 10.1 KB
/
Copy pathcontent-script.js
File metadata and controls
271 lines (239 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/**
* @file content-script.js
* @description This script runs on aistudio.google.com to handle file uploads.
* It intercepts file input changes, renames code files to .txt in-memory,
* and updates the file input to allow uploading to AI Studio.
*/
/**
* A set of default supported file extensions that will be renamed to .txt.
* This makes checking for supported file types efficient.
* @type {Set<string>}
*/
const DEFAULT_EXTENSIONS = new Set([
'.py', '.js', '.ts', '.cpp', '.c', '.cs', '.java', '.go', '.php', '.rs', '.md',
'.json', '.yml', '.yaml', '.html', '.css', '.sh', '.bat', '.pl', '.rb', '.jsx',
'.tsx', '.vue', '.svelte', '.dart', '.kt', '.swift', '.r', '.sql', '.xml',
'.toml', '.ini', '.cfg', '.conf'
]);
/**
* Combined set of default and custom extensions
* @type {Set<string>}
*/
let SUPPORTED_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
/**
* Load custom extensions from storage and update the supported extensions set
*/
function loadCustomExtensions() {
chrome.storage.sync.get('customExtensions', (data) => {
const customExtensions = data.customExtensions || [];
SUPPORTED_EXTENSIONS = new Set([...DEFAULT_EXTENSIONS, ...customExtensions]);
});
}
/**
* Renames a file to have a .txt extension while preserving its content and metadata.
* @param {File} file - The original file object to be renamed.
* @returns {File} A new File object with the .txt extension.
*/
function renameFileToTxt(file) {
const originalName = file.name;
const lastDotIndex = originalName.lastIndexOf('.');
// Handle files with no extension or hidden files starting with a dot.
const baseName = lastDotIndex > 0 ? originalName.substring(0, lastDotIndex) : originalName;
const newName = `${baseName}.txt`;
// Create a new File object with the new name. The content is passed as a blob.
// The original file's lastModified timestamp is preserved.
const newFile = new File([file], newName, {
type: 'text/plain',
lastModified: file.lastModified,
});
return newFile;
}
/**
* Handles the 'change' event on file input elements. It processes the selected
* files, renames them if necessary, and updates the input's file list.
* @param {Event} event - The file input change event.
*/
function handleFileInputChange(event) {
const input = event.target;
if (!input.files || input.files.length === 0) {
return;
}
const originalFiles = Array.from(input.files);
const dataTransfer = new DataTransfer();
let filesModified = false;
const processedFiles = originalFiles.map(file => {
const lastDotIndex = file.name.lastIndexOf('.');
const extension = lastDotIndex > 0 ? file.name.substring(lastDotIndex).toLowerCase() : '';
if (extension && SUPPORTED_EXTENSIONS.has(extension)) {
const newFile = renameFileToTxt(file);
dataTransfer.items.add(newFile);
filesModified = true;
return { original: file.name, newName: newFile.name };
} else {
dataTransfer.items.add(file);
return null;
}
});
if (filesModified) {
// Update the input's files with the renamed files
try {
input.files = dataTransfer.files;
} catch (e) {
// Fallback: Override the files property
Object.defineProperty(input, 'files', {
value: dataTransfer.files,
writable: false,
configurable: true
});
}
// Trigger multiple events to ensure AI Studio detects the change
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
// Add a small delay and trigger again to ensure AI Studio processes it
setTimeout(() => {
input.dispatchEvent(new Event('input', { bubbles: true }));
}, 100);
}
}
/**
* Attaches the file input change listener to all existing file inputs on the page.
* It adds a custom attribute to prevent attaching multiple listeners to the same element.
*/
function attachListenersToExistingInputs() {
const fileInputs = document.querySelectorAll('input[type="file"]');
fileInputs.forEach(input => {
if (!input.dataset.codeUploaderAttached) {
// Listen to multiple events that could indicate file selection
input.addEventListener('change', handleFileInputChange);
input.addEventListener('input', handleFileInputChange);
// Override the files property setter to catch programmatic changes
const originalFilesDescriptor = Object.getOwnPropertyDescriptor(input, 'files') ||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'files');
if (originalFilesDescriptor && originalFilesDescriptor.set) {
Object.defineProperty(input, 'files', {
get: originalFilesDescriptor.get,
set: function(value) {
originalFilesDescriptor.set.call(this, value);
if (value && value.length > 0) {
handleFileInputChange({ target: this });
}
},
configurable: true
});
}
input.dataset.codeUploaderAttached = 'true';
}
});
}
/**
* Observes the DOM for newly added file inputs and attaches listeners to them.
* This is essential for single-page applications where content is loaded dynamically.
*/
function observeForNewInputs() {
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node itself is a file input
if (node.matches('input[type="file"]') && !node.dataset.codeUploaderAttached) {
// Listen to multiple events
node.addEventListener('change', handleFileInputChange);
node.addEventListener('input', handleFileInputChange);
// Override files property setter
const originalFilesDescriptor = Object.getOwnPropertyDescriptor(node, 'files') ||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'files');
if (originalFilesDescriptor && originalFilesDescriptor.set) {
Object.defineProperty(node, 'files', {
get: originalFilesDescriptor.get,
set: function(value) {
originalFilesDescriptor.set.call(this, value);
if (value && value.length > 0) {
handleFileInputChange({ target: this });
}
},
configurable: true
});
}
node.dataset.codeUploaderAttached = 'true';
}
// Check for file inputs within the added node's subtree
const newInputs = node.querySelectorAll('input[type="file"]');
newInputs.forEach(input => {
if (!input.dataset.codeUploaderAttached) {
// Listen to multiple events
input.addEventListener('change', handleFileInputChange);
input.addEventListener('input', handleFileInputChange);
// Override files property setter
const originalFilesDescriptor = Object.getOwnPropertyDescriptor(input, 'files') ||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'files');
if (originalFilesDescriptor && originalFilesDescriptor.set) {
Object.defineProperty(input, 'files', {
get: originalFilesDescriptor.get,
set: function(value) {
originalFilesDescriptor.set.call(this, value);
if (value && value.length > 0) {
handleFileInputChange({ target: this });
}
},
configurable: true
});
}
input.dataset.codeUploaderAttached = 'true';
}
});
}
});
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
/**
* Initializes the content script by attaching listeners and setting up the observer.
* It also listens for messages from the popup, such as enabling/disabling the extension.
*/
function initialize() {
// Load custom extensions first
loadCustomExtensions();
chrome.storage.sync.get('extensionEnabled', (data) => {
if (data.extensionEnabled !== false) { // Enabled by default
attachListenersToExistingInputs();
observeForNewInputs();
// Add global click listener to detect file upload button clicks
document.addEventListener('click', (event) => {
// Check if clicked element or its parent might trigger file upload
const target = event.target;
if (target.matches('button, [role="button"], .upload-btn, [data-testid*="upload"], [aria-label*="upload" i], [aria-label*="attach" i]') ||
target.closest('button, [role="button"], .upload-btn, [data-testid*="upload"], [aria-label*="upload" i], [aria-label*="attach" i]')) {
// Wait a bit for any new file inputs to be created
setTimeout(() => {
attachListenersToExistingInputs();
}, 100);
}
});
}
});
// Listen for messages from the popup to toggle the extension's functionality
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'toggleExtension') {
if (message.enabled) {
attachListenersToExistingInputs();
observeForNewInputs();
}
sendResponse({ success: true });
} else if (message.action === 'updateExtensions') {
// Update the supported extensions when custom extensions are modified
const customExtensions = message.customExtensions || [];
SUPPORTED_EXTENSIONS = new Set([...DEFAULT_EXTENSIONS, ...customExtensions]);
sendResponse({ success: true });
}
});
}
// Only initialize if we're on AI Studio
if (window.location.hostname.includes('aistudio.google.com')) {
initialize();
}