tasty_chrome/images/icon128.png
tasty_chrome/images/icon128.png
This is a binary file and will not be displayed.
tasty_chrome/images/icon16.png
tasty_chrome/images/icon16.png
This is a binary file and will not be displayed.
tasty_chrome/images/icon32.png
tasty_chrome/images/icon32.png
This is a binary file and will not be displayed.
tasty_chrome/images/icon48.png
tasty_chrome/images/icon48.png
This is a binary file and will not be displayed.
+4
-2
tasty_chrome/manifest.json
+4
-2
tasty_chrome/manifest.json
+300
tasty_chrome/phoenix_simple.js
+300
tasty_chrome/phoenix_simple.js
···
1
+
/**
2
+
* Simplified Phoenix WebSocket Client
3
+
* A lightweight alternative to the full Phoenix.js client
4
+
*/
5
+
6
+
class PhoenixSocket {
7
+
constructor(endpoint, opts = {}) {
8
+
this.endpoint = endpoint;
9
+
this.params = opts.params || {};
10
+
this.timeout = opts.timeout || 10000;
11
+
this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000;
12
+
this.socket = null;
13
+
this.channels = {};
14
+
this.messageCallbacks = [];
15
+
this.connected = false;
16
+
this.heartbeatTimer = null;
17
+
this.pendingHeartbeatRef = null;
18
+
this.ref = 0;
19
+
20
+
// Bind methods to maintain context
21
+
this.handleOpen = this.handleOpen.bind(this);
22
+
this.handleMessage = this.handleMessage.bind(this);
23
+
this.handleError = this.handleError.bind(this);
24
+
this.handleClose = this.handleClose.bind(this);
25
+
}
26
+
27
+
connect() {
28
+
if (this.socket) {
29
+
return;
30
+
}
31
+
32
+
const url = this.buildUrl();
33
+
this.socket = new WebSocket(url);
34
+
35
+
this.socket.onopen = this.handleOpen;
36
+
this.socket.onmessage = this.handleMessage;
37
+
this.socket.onerror = this.handleError;
38
+
this.socket.onclose = this.handleClose;
39
+
}
40
+
41
+
disconnect() {
42
+
if (this.socket) {
43
+
this.stopHeartbeat();
44
+
this.socket.close();
45
+
this.socket = null;
46
+
this.connected = false;
47
+
}
48
+
}
49
+
50
+
handleOpen() {
51
+
console.log(`Connected to ${this.endpoint}`);
52
+
this.connected = true;
53
+
this.startHeartbeat();
54
+
}
55
+
56
+
handleMessage(event) {
57
+
try {
58
+
const message = JSON.parse(event.data);
59
+
console.log('Received message:', message);
60
+
61
+
// Handle heartbeat responses
62
+
if (message.ref === this.pendingHeartbeatRef) {
63
+
this.pendingHeartbeatRef = null;
64
+
clearTimeout(this.heartbeatTimer);
65
+
this.startHeartbeat();
66
+
return;
67
+
}
68
+
69
+
// Handle channel messages
70
+
const { topic, event: eventName, payload, ref } = message;
71
+
72
+
// Check if this is a response to a channel join
73
+
if (eventName === 'phx_reply' && this.channels[topic]) {
74
+
if (payload.status === 'ok') {
75
+
this.channels[topic].joined = true;
76
+
if (this.channels[topic].onJoin) {
77
+
this.channels[topic].onJoin(payload);
78
+
}
79
+
} else if (payload.status === 'error') {
80
+
if (this.channels[topic].onError) {
81
+
this.channels[topic].onError(payload);
82
+
}
83
+
}
84
+
}
85
+
86
+
// Forward to channel callbacks
87
+
if (this.channels[topic] && this.channels[topic].callbacks[eventName]) {
88
+
this.channels[topic].callbacks[eventName].forEach(callback => {
89
+
callback(payload, ref);
90
+
});
91
+
}
92
+
93
+
// Forward to general message callbacks
94
+
this.messageCallbacks.forEach(callback => {
95
+
callback(message);
96
+
});
97
+
} catch (error) {
98
+
console.error('Error parsing message:', error);
99
+
}
100
+
}
101
+
102
+
handleError(error) {
103
+
console.error('WebSocket error:', error);
104
+
this.stopHeartbeat();
105
+
}
106
+
107
+
handleClose(event) {
108
+
console.log(`WebSocket closed: ${event.code} ${event.reason}`);
109
+
this.connected = false;
110
+
this.stopHeartbeat();
111
+
112
+
// Clean up channels
113
+
Object.keys(this.channels).forEach(topic => {
114
+
this.channels[topic].joined = false;
115
+
});
116
+
}
117
+
118
+
channel(topic, params = {}) {
119
+
if (!this.channels[topic]) {
120
+
this.channels[topic] = {
121
+
topic,
122
+
params,
123
+
joined: false,
124
+
callbacks: {},
125
+
onJoin: null,
126
+
onError: null
127
+
};
128
+
}
129
+
130
+
return {
131
+
join: () => this.joinChannel(topic),
132
+
leave: () => this.leaveChannel(topic),
133
+
on: (event, callback) => this.on(topic, event, callback),
134
+
push: (event, payload) => this.push(topic, event, payload),
135
+
onJoin: (callback) => { this.channels[topic].onJoin = callback; },
136
+
onError: (callback) => { this.channels[topic].onError = callback; }
137
+
};
138
+
}
139
+
140
+
joinChannel(topic) {
141
+
if (!this.channels[topic]) {
142
+
throw new Error(`No channel found for topic: ${topic}`);
143
+
}
144
+
145
+
const ref = this.makeRef();
146
+
const message = {
147
+
topic,
148
+
event: 'phx_join',
149
+
payload: this.channels[topic].params,
150
+
ref
151
+
};
152
+
153
+
this.send(message);
154
+
155
+
return new Promise((resolve, reject) => {
156
+
const timeoutId = setTimeout(() => {
157
+
reject(new Error('Join timeout'));
158
+
}, this.timeout);
159
+
160
+
this.channels[topic].onJoin = (payload) => {
161
+
clearTimeout(timeoutId);
162
+
resolve(payload);
163
+
};
164
+
165
+
this.channels[topic].onError = (payload) => {
166
+
clearTimeout(timeoutId);
167
+
reject(new Error(payload.response?.reason || 'Join failed'));
168
+
};
169
+
});
170
+
}
171
+
172
+
leaveChannel(topic) {
173
+
if (!this.channels[topic]) {
174
+
return;
175
+
}
176
+
177
+
const ref = this.makeRef();
178
+
const message = {
179
+
topic,
180
+
event: 'phx_leave',
181
+
payload: {},
182
+
ref
183
+
};
184
+
185
+
this.send(message);
186
+
delete this.channels[topic];
187
+
}
188
+
189
+
on(topic, event, callback) {
190
+
if (!this.channels[topic]) {
191
+
throw new Error(`No channel found for topic: ${topic}`);
192
+
}
193
+
194
+
if (!this.channels[topic].callbacks[event]) {
195
+
this.channels[topic].callbacks[event] = [];
196
+
}
197
+
198
+
this.channels[topic].callbacks[event].push(callback);
199
+
200
+
return () => {
201
+
this.channels[topic].callbacks[event] = this.channels[topic].callbacks[event].filter(
202
+
cb => cb !== callback
203
+
);
204
+
};
205
+
}
206
+
207
+
push(topic, event, payload = {}) {
208
+
if (!this.channels[topic]) {
209
+
throw new Error(`No channel found for topic: ${topic}`);
210
+
}
211
+
212
+
const ref = this.makeRef();
213
+
const message = {
214
+
topic,
215
+
event,
216
+
payload,
217
+
ref
218
+
};
219
+
220
+
this.send(message);
221
+
222
+
return {
223
+
receive: (status, callback) => {
224
+
const replyEvent = `phx_reply`;
225
+
226
+
const removeListener = this.on(topic, replyEvent, (payload, msgRef) => {
227
+
if (msgRef === ref && payload.status === status) {
228
+
removeListener();
229
+
callback(payload.response);
230
+
}
231
+
});
232
+
}
233
+
};
234
+
}
235
+
236
+
onMessage(callback) {
237
+
this.messageCallbacks.push(callback);
238
+
return () => {
239
+
this.messageCallbacks = this.messageCallbacks.filter(cb => cb !== callback);
240
+
};
241
+
}
242
+
243
+
startHeartbeat() {
244
+
this.stopHeartbeat();
245
+
this.heartbeatTimer = setTimeout(() => {
246
+
if (this.connected) {
247
+
this.pendingHeartbeatRef = this.makeRef();
248
+
this.push('phoenix', 'heartbeat', {});
249
+
}
250
+
}, this.heartbeatIntervalMs);
251
+
}
252
+
253
+
stopHeartbeat() {
254
+
clearTimeout(this.heartbeatTimer);
255
+
this.heartbeatTimer = null;
256
+
this.pendingHeartbeatRef = null;
257
+
}
258
+
259
+
makeRef() {
260
+
this.ref += 1;
261
+
return this.ref.toString();
262
+
}
263
+
264
+
send(message) {
265
+
if (!this.connected) {
266
+
console.warn('Tried to send message while disconnected', message);
267
+
return false;
268
+
}
269
+
270
+
try {
271
+
this.socket.send(JSON.stringify(message));
272
+
return true;
273
+
} catch (error) {
274
+
console.error('Error sending message:', error);
275
+
return false;
276
+
}
277
+
}
278
+
279
+
buildUrl() {
280
+
// Append websocket suffix and params
281
+
let url = this.endpoint;
282
+
if (!url.endsWith('/websocket')) {
283
+
url = `${url}/websocket`;
284
+
}
285
+
286
+
// Add params as query string
287
+
if (Object.keys(this.params).length > 0) {
288
+
const params = new URLSearchParams();
289
+
Object.entries(this.params).forEach(([key, value]) => {
290
+
params.append(key, value);
291
+
});
292
+
url = `${url}?${params.toString()}`;
293
+
}
294
+
295
+
return url;
296
+
}
297
+
}
298
+
299
+
// Export globally
300
+
window.PhoenixSocket = PhoenixSocket;
+1
-1
tasty_chrome/popup.html
+1
-1
tasty_chrome/popup.html
+101
-23
tasty_chrome/popup.js
+101
-23
tasty_chrome/popup.js
···
18
18
urlInput.value = currentTab.url || '';
19
19
});
20
20
21
+
// Check for quick bookmark data from context menu
22
+
chrome.storage.local.get(['quickBookmark'], (result) => {
23
+
if (result.quickBookmark) {
24
+
titleInput.value = result.quickBookmark.title || '';
25
+
urlInput.value = result.quickBookmark.url || '';
26
+
// Clear the stored data
27
+
chrome.storage.local.remove(['quickBookmark']);
28
+
}
29
+
});
30
+
21
31
// Load saved token from storage
22
32
chrome.storage.local.get(['token'], (result) => {
23
33
if (result.token) {
···
30
40
const token = tokenInput.value.trim();
31
41
if (token) {
32
42
chrome.storage.local.set({ token });
43
+
console.log('Token saved to storage');
33
44
}
34
45
});
35
46
···
96
107
message.textContent = '';
97
108
98
109
try {
99
-
// Connect to Phoenix socket
100
-
const socket = new Phoenix.Socket("ws://localhost:4000/socket", {
110
+
console.log('Connecting to Phoenix socket...');
111
+
112
+
// Connect to Phoenix socket with our simplified client
113
+
const socket = new PhoenixSocket("ws://localhost:4000/socket", {
101
114
params: { token }
102
115
});
103
116
104
117
socket.connect();
105
118
106
-
// Join the client channel
107
-
const channel = socket.channel(`bookmark:client:${token}`);
119
+
// Create a timeout to handle connection failures
120
+
const connectionTimeout = setTimeout(() => {
121
+
showMessage('Connection timeout. Server not responding.', 'error');
122
+
resetForm();
123
+
}, 5000);
108
124
109
-
channel.join()
110
-
.receive("ok", resp => {
111
-
console.log("Joined successfully", resp);
112
-
sendBookmark(channel);
113
-
})
114
-
.receive("error", resp => {
115
-
console.error("Unable to join", resp);
116
-
showMessage(`Error connecting: ${resp.reason || 'Unauthorized'}`, 'error');
117
-
resetForm();
118
-
});
125
+
// Wait for connection to establish
126
+
setTimeout(() => {
127
+
if (socket.connected) {
128
+
clearTimeout(connectionTimeout);
129
+
130
+
// Join the client channel
131
+
const channelTopic = `bookmark:client:${token}`;
132
+
console.log(`Joining channel: ${channelTopic}`);
133
+
const channel = socket.channel(channelTopic);
134
+
135
+
try {
136
+
// Join the channel
137
+
channel.join()
138
+
.then(() => {
139
+
console.log('Channel joined successfully');
140
+
sendBookmark(socket, channel);
141
+
})
142
+
.catch(error => {
143
+
console.error('Error joining channel:', error);
144
+
showMessage(`Error joining channel: ${error.message}`, 'error');
145
+
resetForm();
146
+
});
119
147
148
+
// Add error handler
149
+
channel.onError((resp) => {
150
+
console.error('Channel error:', resp);
151
+
showMessage(`Channel error: ${resp.reason || 'Unknown error'}`, 'error');
152
+
resetForm();
153
+
});
154
+
} catch (error) {
155
+
console.error('Error setting up channel:', error);
156
+
showMessage(`Error: ${error.message}`, 'error');
157
+
resetForm();
158
+
}
159
+
} else {
160
+
clearTimeout(connectionTimeout);
161
+
console.error('WebSocket not connected');
162
+
showMessage('Could not establish WebSocket connection', 'error');
163
+
resetForm();
164
+
}
165
+
}, 1000);
120
166
} catch (error) {
121
167
console.error('Connection error:', error);
122
168
showMessage(`Connection error: ${error.message}`, 'error');
···
124
170
}
125
171
});
126
172
127
-
function sendBookmark(channel) {
173
+
function sendBookmark(socket, channel) {
174
+
console.log('Preparing to send bookmark data');
175
+
128
176
const payload = {
129
177
title: titleInput.value,
130
178
url: urlInput.value,
···
132
180
tags: tags
133
181
};
134
182
135
-
channel.push("bookmark:create", payload)
136
-
.receive("ok", resp => {
137
-
console.log("Bookmark created", resp);
183
+
console.log('Bookmark payload:', payload);
184
+
185
+
try {
186
+
const pushResult = channel.push('bookmark:create', payload);
187
+
188
+
pushResult.receive('ok', (resp) => {
189
+
console.log('Bookmark created successfully:', resp);
138
190
showMessage('Bookmark saved successfully!', 'success');
139
-
setTimeout(() => window.close(), 1500);
140
-
})
141
-
.receive("error", resp => {
142
-
console.error("Failed to create bookmark", resp);
143
-
showMessage(`Error: ${formatErrors(resp.errors)}`, 'error');
191
+
192
+
// Close the popup after success
193
+
setTimeout(() => {
194
+
socket.disconnect();
195
+
window.close();
196
+
}, 1500);
197
+
});
198
+
199
+
pushResult.receive('error', (resp) => {
200
+
console.error('Failed to create bookmark:', resp);
201
+
showMessage(`Error: ${formatErrors(resp)}`, 'error');
202
+
socket.disconnect();
144
203
resetForm();
145
204
});
205
+
206
+
// Set a timeout in case we don't get a response
207
+
setTimeout(() => {
208
+
if (saveButton.disabled) {
209
+
console.warn('No response received from server');
210
+
showMessage('No response from server. Your bookmark may or may not have been saved.', 'error');
211
+
socket.disconnect();
212
+
resetForm();
213
+
}
214
+
}, 10000);
215
+
} catch (error) {
216
+
console.error('Error sending bookmark:', error);
217
+
showMessage(`Error: ${error.message}`, 'error');
218
+
socket.disconnect();
219
+
resetForm();
220
+
}
146
221
}
147
222
148
223
function formatErrors(errors) {
149
224
if (!errors) return 'Unknown error';
150
225
226
+
if (typeof errors === 'string') return errors;
227
+
151
228
return Object.entries(errors)
152
229
.map(([field, messages]) => `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`)
153
230
.join('; ');
···
156
233
function showMessage(text, type) {
157
234
message.textContent = text;
158
235
message.className = type;
236
+
console.log(`Message (${type}): ${text}`);
159
237
}
160
238
161
239
function resetForm() {