├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── README.zh.md
├── css
└── TweetTrack.css
├── html
├── about.html
└── sidepanel.html
├── images
├── bmc_qr.png
└── twitter.png
├── js
├── content-script.js
├── scripts.js
└── service-worker.js
├── manifest.json
└── privacy-policy.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "codeQL.githubDatabase.download": "never"
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Camaro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TweetTrace
2 |
3 | **TweetTrace** is a free and open-source Chrome extension that records tweets you visit, providing seamless search and filtering functionalities to help you navigate your Twitter history while keeping your data secure and private.
4 |
5 | ## Features
6 |
7 | - **Local Storage**: All tweet data is stored locally on your device using your browser's indexedDB, ensuring that your data never leaves your device.
8 |
9 | - **Privacy-Focused**: TweetTrace does not analyze, extract, or share your data, giving you complete control and privacy over your information.
10 |
11 | - **Search and Filter**: Easily search and filter through recorded tweets to find exactly what you're looking for.
12 |
13 | - **Open Source**: As an open-source project, TweetTrace invites collaboration and contributions from developers and users who want to enhance the tool further.
14 |
15 | ## Installation
16 |
17 | ### Chrome Web Store
18 |
19 | 1. Visit the [Chrome Web Store](https://chrome.google.com/webstore) to install the extension directly.
20 | 2. Search for "TweetTrace" in the store.
21 | 3. Click "Add to Chrome" to install the extension.
22 |
23 | ### Developer Mode
24 |
25 | 1. Download the TweetTrace source code from the GitHub repository.
26 | 2. Unzip the downloaded file to a location on your computer.
27 | 3. Open Google Chrome and navigate to `chrome://extensions/`.
28 | 4. Enable "Developer mode" by toggling the switch in the top right corner.
29 | 5. Click "Load unpacked" and select the unzipped TweetTrace folder.
30 | 6. The extension should now appear in your Chrome browser.
31 |
32 | ## Usage
33 |
34 | ### Automatic Recording
35 | - Once installed, TweetTrace will automatically start recording tweets you visit.
36 | - Use the browser action icon to access your recorded tweets and utilize the search and filtering options.
37 |
38 | ## Update Log
39 |
40 | Version 1.0.0 (2024-08-21):
41 | - Initial release of TweetTrace
42 | - Implemented local storage for tweet logs
43 | - Added search functionality with result highlighting
44 | - Basic tweet recording functionality
45 |
46 | ## License
47 |
48 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
49 |
50 | ## Support the Project
51 |
52 | If you find TweetTrace helpful, consider buying me a coffee! Your support helps maintain and improve this project.
53 |
54 |
55 |
56 |
57 |
58 | Thank you for your support!
59 |
60 | ---
61 |
62 | Feel free to reach out if you have any questions or feedback!
63 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | # TweetTrace
2 |
3 | **TweetTrace** 是一个免费开源的 Chrome 扩展程序,能够记录您访问过的推文,并提供无缝的搜索和过滤功能,帮助您浏览 Twitter 历史记录,同时确保您的数据安全和隐私。
4 |
5 | ## 功能特点
6 |
7 | - **本地存储**:所有推文数据都使用浏览器的 indexedDB 存储在您的设备上,确保您的数据永远不会离开您的设备。
8 |
9 | - **注重隐私**:TweetTrace 不会分析、提取或分享您的数据,让您完全控制并保护您的信息。
10 |
11 | - **搜索和过滤**:轻松搜索和过滤已记录的推文,准确找到您所需的内容。
12 |
13 | - **开源项目**:作为一个开源项目,TweetTrace 欢迎开发者和用户的协作与贡献,以进一步改进这个工具。
14 |
15 | ## 安装
16 |
17 | ### Chrome 网上应用店
18 |
19 | 1. 访问 [Chrome 网上应用店](https://chrome.google.com/webstore) 直接安装扩展程序。
20 | 2. 在商店中搜索 "TweetTrace"。
21 | 3. 点击 "添加至 Chrome" 安装扩展程序。
22 |
23 | ### 开发者模式
24 |
25 | 1. 从 GitHub 仓库下载 TweetTrace 源代码。
26 | 2. 将下载的文件解压到您计算机上的某个位置。
27 | 3. 打开 Google Chrome 并导航至 `chrome://extensions/`。
28 | 4. 通过切换右上角的开关启用 "开发者模式"。
29 | 5. 点击 "加载已解压的扩展程序" 并选择解压后的 TweetTrace 文件夹。
30 | 6. 该扩展程序现在应该出现在您的 Chrome 浏览器中。
31 |
32 | ## 使用方法
33 |
34 | ### 自动记录
35 | - 安装完成后,TweetTrace 将自动开始记录您访问的推文。
36 | - 使用浏览器操作图标访问您记录的推文,并使用搜索和过滤选项。
37 |
38 | ## 更新日志
39 |
40 | 版本 1.0.0 (2024-08-21):
41 | - TweetTrace 初始发布
42 | - 实现推文日志的本地存储
43 | - 添加搜索功能并高亮显示结果
44 | - 基本的推文记录功能
45 |
46 | ## 许可证
47 |
48 | 本项目采用 MIT 许可证 - 详情请见 [LICENSE](LICENSE) 文件。
49 |
50 | ## 支持项目
51 |
52 | 如果您觉得 TweetTrace 有帮助,不妨给我买杯咖啡!您的支持有助于维护和改进这个项目。
53 |
54 |
55 |
56 |
57 |
58 | 感谢您的支持!
59 |
60 | ---
61 |
62 | 如果您有任何问题或反馈,欢迎随时联系我们!
63 |
--------------------------------------------------------------------------------
/css/TweetTrack.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
3 | margin: 0;
4 | padding: 0;
5 | background-color: #ffffff;
6 | color: #37352f;
7 | line-height: 1.4;
8 | font-size: 14px;
9 | }
10 |
11 | .topbar {
12 | display: flex;
13 | align-items: center;
14 | padding: 8px 16px;
15 | background-color: #ffffff;
16 | border-bottom: 1px solid #e0e0e0;
17 | position: sticky;
18 | top: 0;
19 | z-index: 1000;
20 | }
21 |
22 | .logo {
23 | display: flex;
24 | align-items: center;
25 | font-size: 18px;
26 | font-weight: bold;
27 | color: #37352f;
28 | margin-right: 16px;
29 | }
30 | .logo-link {
31 | text-decoration: none;
32 | color: inherit;
33 | }
34 | .logo img {
35 | height: 24px;
36 | margin-right: 8px;
37 | }
38 |
39 | .search-container {
40 | flex-grow: 1;
41 | }
42 |
43 | .search-box {
44 | width: 100%;
45 | padding: 5px 8px;
46 | font-size: 13px;
47 | border: 1px solid #e0e0e0;
48 | border-radius: 3px;
49 | outline: none;
50 | box-sizing: border-box;
51 | background-color: #f7f7f7;
52 | }
53 |
54 | .search-box:focus {
55 | background-color: #fff;
56 | border-color: #b3b3b3;
57 | }
58 |
59 | .search-results {
60 | max-width: 550px;
61 | margin: 16px auto;
62 | padding: 0 16px;
63 | }
64 |
65 | .result-item {
66 | margin-bottom: 16px;
67 | padding-bottom: 12px;
68 | border-bottom: 1px solid #e0e0e0;
69 | }
70 |
71 | .result-item:last-child {
72 | border-bottom: none;
73 | margin-bottom: 0;
74 | padding-bottom: 0;
75 | }
76 |
77 | .header-container {
78 | display: flex;
79 | justify-content: space-between;
80 | align-items: flex-start;
81 | margin-bottom: 8px;
82 | }
83 |
84 | .user-info {
85 | display: flex;
86 | align-items: flex-start;
87 | }
88 |
89 | .user-avatar {
90 | width: 48px;
91 | height: 48px;
92 | border-radius: 50%;
93 | margin-right: 12px;
94 | }
95 |
96 | .user-name-id {
97 | display: flex;
98 | flex-direction: column;
99 | }
100 |
101 | .user-name {
102 | font-weight: 600;
103 | font-size: 15px;
104 | margin-bottom: 2px;
105 | }
106 |
107 | .user-id {
108 | color: #787774;
109 | font-size: 13px;
110 | }
111 |
112 | .meta-info {
113 | display: flex;
114 | align-items: center;
115 | font-size: 13px;
116 | color: #657786;
117 | }
118 |
119 | .tweet-time {
120 | margin-right: 8px;
121 | }
122 |
123 | .tweet-link {
124 | text-decoration: none;
125 | color: #1DA1F2;
126 | font-size: 16px;
127 | }
128 |
129 | .tweet-content {
130 | font-size: 14px;
131 | color: #37352f;
132 | margin: 0;
133 | word-wrap: break-word;
134 | overflow-wrap: break-word;
135 | white-space: pre-wrap;
136 | max-width: 100%;
137 | }
138 |
139 | .highlight-text {
140 | background-color: yellow;
141 | padding: 2px;
142 | border-radius: 2px;
143 | }
144 |
145 | .clickable {
146 | cursor: pointer;
147 | }
148 |
149 | .user-name, .user-id {
150 | text-decoration: none;
151 | color: inherit;
152 | }
--------------------------------------------------------------------------------
/html/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | TweetTrace - Track Your Twitter History
8 |
229 |
230 |
231 |
232 |
233 |
234 |
264 |
265 |
266 |
267 |
268 |
269 |
What is TweetTrace?
270 |
271 | TweetTrace is a free and open-source Chrome extension that records tweets you
272 | visit,
273 | providing seamless search and filtering functionalities to help you navigate your Twitter
274 | history while
275 | keeping your data secure and private.
276 |
277 |
278 |
Features
279 |
280 | Local Storage: All tweet data is stored locally on your device using your
281 | browser's
282 | localStorage, ensuring that your data never leaves your device.
283 | Privacy-Focused: TweetTrace does not analyze, extract, or share your data,
284 | giving
285 | you complete control and privacy over your information.
286 | Search and Filter: Easily search and filter through recorded tweets to find
287 | exactly
288 | what you're looking for.
289 | Open Source: As an open-source project, TweetTrace invites collaboration
290 | and
291 | contributions from developers and users who want to enhance the tool further.
292 |
293 |
294 |
Update Log
295 |
296 | Version 1.0.0 (2024-08-21):
297 |
298 | Initial release of TweetTrace
299 | Implemented local storage for tweet logs
300 | Added search functionality with result highlighting
301 | Basic tweet recording functionality
302 |
303 |
304 |
305 |
306 |
307 |
308 |
311 |
312 |
313 |
314 |
--------------------------------------------------------------------------------
/html/sidepanel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | TweetTrace Search
7 |
8 |
9 |
10 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/images/bmc_qr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CstCamaro/TweetTrace/aeab9e39b91df3de5556772fcd59af59b07ab219/images/bmc_qr.png
--------------------------------------------------------------------------------
/images/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CstCamaro/TweetTrace/aeab9e39b91df3de5556772fcd59af59b07ab219/images/twitter.png
--------------------------------------------------------------------------------
/js/content-script.js:
--------------------------------------------------------------------------------
1 | // Create a Set to keep track of seen tweet URLs
2 | const seenTweetUrls = new Set();
3 |
4 | function extractTweetData() {
5 | const tweetElements = document.querySelectorAll('[data-testid="tweet"]');
6 |
7 | tweetElements.forEach(tweetElement => {
8 | const tweetTextElement = tweetElement.querySelector('[data-testid="tweetText"]');
9 | if (!tweetTextElement) return;
10 |
11 | let tweetContentArray = [];
12 | tweetTextElement.childNodes.forEach(node => {
13 | if (node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && (node.tagName === 'SPAN' || node.tagName === 'A'))) {
14 | tweetContentArray.push(node.textContent);
15 | }
16 | });
17 |
18 | let tweetText = tweetContentArray.join('').trim();
19 | const tweetLinkElement = tweetElement.querySelector('a[dir="ltr"]');
20 |
21 | if (tweetLinkElement) {
22 | const href = tweetLinkElement.getAttribute('href');
23 | const regex = /^\/([^\/]+)(\/status\/\d+)$/;
24 | const match = href.match(regex);
25 |
26 | if (match) {
27 | const userId = match[1];
28 | const tweetPath = match[2];
29 | const fullTweetUrl = `https://x.com/${userId}${tweetPath}`;
30 |
31 | // Check if this tweet URL has already been processed
32 | if (!seenTweetUrls.has(fullTweetUrl)) {
33 |
34 | // Extract username (nickname)
35 | let username = null;
36 | try {
37 | const userNameElement = tweetElement.querySelector('[data-testid="User-Name"]');
38 | if (userNameElement) {
39 | const nameElement = userNameElement.querySelector('div[dir="ltr"] > span > span');
40 | if (nameElement) {
41 | username = nameElement.textContent.trim();
42 | }
43 | }
44 | } catch (error) {
45 | console.error('Error extracting username:', error);
46 | }
47 |
48 | // Extract time
49 | let tweetTime = null;
50 | try {
51 | const timeElement = tweetElement.querySelector('time');
52 | if (timeElement) {
53 | tweetTime = timeElement.getAttribute('datetime');
54 | }
55 | } catch (error) {
56 | console.error('Error extracting tweet time:', error);
57 | }
58 |
59 | // Extract avatar URL
60 | let avatarUrl = null;
61 | try {
62 | const imgElement = tweetElement.querySelector('[data-testid="Tweet-User-Avatar"] img');
63 | if (imgElement) {
64 | avatarUrl = imgElement.src;
65 | }
66 | } catch (error) {
67 | console.error('Error extracting avatar URL:', error);
68 | }
69 |
70 | const tweetData = {
71 | id: `${userId}_${tweetPath}`,
72 | time: tweetTime,
73 | username: username,
74 | url: fullTweetUrl,
75 | text: tweetText,
76 | avatarUrl: avatarUrl,
77 | userId: userId
78 | };
79 |
80 | // Add the URL to the Set
81 | seenTweetUrls.add(fullTweetUrl);
82 |
83 | // Send message to background service worker
84 | // console.log('Sending message to background script:', tweetData);
85 | chrome.runtime.sendMessage({ type: 'storeTweetData', tweetData: tweetData });
86 | }
87 | }
88 | }
89 | });
90 | }
91 |
92 | window.addEventListener('scroll', () => {
93 | if (!this.scrollTimer) {
94 | this.scrollTimer = setTimeout(() => {
95 | this.scrollTimer = null;
96 | extractTweetData();
97 | }, 300);
98 | }
99 | });
100 |
101 | // Call to capture tweets already visible on the page
102 | extractTweetData();
--------------------------------------------------------------------------------
/js/scripts.js:
--------------------------------------------------------------------------------
1 | let currentPage = 1;
2 | const tweetsPerPage = 10;
3 | let allTweets = [];
4 | let isLoading = false;
5 | let currentSearchQuery = '';
6 |
7 | function applyHighlight(element, query) {
8 | if (!query) return;
9 |
10 | const highlightClass = 'highlight-text';
11 | const text = element.textContent;
12 | const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
13 |
14 | let match;
15 | let lastIndex = 0;
16 | const fragments = [];
17 |
18 | while ((match = regex.exec(text)) !== null) {
19 | if (lastIndex !== match.index) {
20 | fragments.push(document.createTextNode(text.slice(lastIndex, match.index)));
21 | }
22 | const span = document.createElement('span');
23 | span.textContent = match[0];
24 | span.className = highlightClass;
25 | fragments.push(span);
26 | lastIndex = regex.lastIndex;
27 | }
28 |
29 | if (lastIndex < text.length) {
30 | fragments.push(document.createTextNode(text.slice(lastIndex)));
31 | }
32 |
33 | element.textContent = '';
34 | fragments.forEach(fragment => element.appendChild(fragment));
35 | }
36 |
37 | // Function to apply highlighting to results
38 | function applyHighlightToResults(query) {
39 | document.querySelectorAll('.result-item').forEach(item => {
40 | applyHighlight(item.querySelector('.user-name'), query);
41 | applyHighlight(item.querySelector('.user-id'), query);
42 | applyHighlight(item.querySelector('.tweet-content'), query);
43 | });
44 | }
45 |
46 | // Function: Open IndexedDB database
47 | function openIndexedDB() {
48 | return new Promise((resolve, reject) => {
49 | const request = indexedDB.open('TweetTraceDB', 1);
50 | request.onerror = event => reject('IndexedDB error: ' + event.target.error);
51 | request.onsuccess = event => resolve(event.target.result);
52 | request.onupgradeneeded = event => {
53 | const db = event.target.result;
54 | db.createObjectStore('tweets', { keyPath: 'id' });
55 | };
56 | });
57 | }
58 |
59 | // Function: Read all tweets from IndexedDB
60 | function getAllTweets() {
61 | return new Promise((resolve, reject) => {
62 | openIndexedDB().then(db => {
63 | const transaction = db.transaction(['tweets'], 'readonly');
64 | const store = transaction.objectStore('tweets');
65 | const request = store.getAll();
66 |
67 | request.onerror = event => reject('Error fetching tweets: ' + event.target.error);
68 | request.onsuccess = event => resolve(event.target.result);
69 | }).catch(error => reject(error));
70 | });
71 | }
72 |
73 | // Function: Create HTML for a single search result item
74 | function createResultItemHTML(result) {
75 | const tweetTime = new Date(result.time).toLocaleString();
76 |
77 | const resultItem = document.createElement('div');
78 | resultItem.className = 'result-item';
79 |
80 | // User info (left side)
81 | const userInfo = document.createElement('div');
82 | userInfo.className = 'user-info';
83 |
84 | const avatar = document.createElement('img');
85 | avatar.src = result.avatarUrl || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
86 | avatar.alt = 'User Avatar';
87 | avatar.className = 'user-avatar clickable';
88 |
89 | const userNameId = document.createElement('div');
90 | userNameId.className = 'user-name-id';
91 |
92 | const userName = document.createElement('div');
93 | userName.className = 'user-name clickable';
94 | userName.textContent = result.username;
95 |
96 | const userId = document.createElement('div');
97 | userId.className = 'user-id clickable';
98 | userId.textContent = `@${result.userId}`;
99 |
100 | userNameId.appendChild(userName);
101 | userNameId.appendChild(userId);
102 | userInfo.appendChild(avatar);
103 | userInfo.appendChild(userNameId);
104 |
105 | // Add click event listeners
106 | [avatar, userName, userId].forEach(element => {
107 | element.addEventListener('click', () => {
108 | window.open(`https://x.com/${result.userId}`, '_blank');
109 | });
110 | });
111 |
112 | // Meta info (right side)
113 | const metaInfo = document.createElement('div');
114 | metaInfo.className = 'meta-info';
115 |
116 | const tweetTimeElement = document.createElement('span');
117 | tweetTimeElement.className = 'tweet-time';
118 | tweetTimeElement.textContent = tweetTime;
119 |
120 | const tweetLink = document.createElement('span');
121 | tweetLink.className = 'tweet-link clickable';
122 | tweetLink.textContent = '🔗';
123 | tweetLink.addEventListener('click', () => {
124 | window.open(result.url, '_blank');
125 | });
126 |
127 | metaInfo.appendChild(tweetTimeElement);
128 | metaInfo.appendChild(tweetLink);
129 |
130 | // Header container
131 | const headerContainer = document.createElement('div');
132 | headerContainer.className = 'header-container';
133 | headerContainer.appendChild(userInfo);
134 | headerContainer.appendChild(metaInfo);
135 |
136 | // Tweet content
137 | const tweetContent = document.createElement('p');
138 | tweetContent.className = 'tweet-content';
139 | tweetContent.textContent = result.text;
140 |
141 | // Assemble all elements
142 | resultItem.appendChild(headerContainer);
143 | resultItem.appendChild(tweetContent);
144 |
145 | return resultItem;
146 | }
147 |
148 | function appendSearchResults(newResults) {
149 | const searchResultsContainer = document.getElementById('searchResults');
150 | newResults.forEach(result => {
151 | const resultElement = createResultItemHTML(result);
152 | searchResultsContainer.appendChild(resultElement);
153 | });
154 | }
155 |
156 | // Function: Render search results
157 | function renderSearchResults(searchResults) {
158 | const searchResultsContainer = document.getElementById('searchResults');
159 |
160 | // Clear previous results
161 | while (searchResultsContainer.firstChild) {
162 | searchResultsContainer.removeChild(searchResultsContainer.firstChild);
163 | }
164 |
165 | if (searchResults.length === 0) {
166 | const noResultsMessage = document.createElement('p');
167 | noResultsMessage.textContent = 'No results found.';
168 | searchResultsContainer.appendChild(noResultsMessage);
169 | } else {
170 | appendSearchResults(searchResults);
171 | }
172 | }
173 |
174 | // Function: Search tweets
175 | function searchTweets(query) {
176 | currentSearchQuery = query; // Update current search query
177 | currentPage = 1; // Reset page number
178 | const filteredTweets = allTweets.filter(tweet => {
179 | try {
180 | const lowercaseQuery = query.toLowerCase();
181 | return (
182 | (tweet.text && tweet.text.toLowerCase().includes(lowercaseQuery)) ||
183 | (tweet.username && tweet.username.toLowerCase().includes(lowercaseQuery)) ||
184 | (tweet.userId && tweet.userId.toLowerCase().includes(lowercaseQuery))
185 | );
186 | } catch (error) {
187 | console.warn('Error processing tweet:', error, tweet);
188 | return false;
189 | }
190 | });
191 | renderSearchResults(filteredTweets.slice(0, tweetsPerPage));
192 |
193 | // Apply highlighting immediately after rendering
194 | applyHighlightToResults(query);
195 | }
196 |
197 | // Function: Initialize search functionality
198 | function initializeSearch() {
199 | const searchInput = document.querySelector('.search-box');
200 | searchInput.addEventListener('input', debounce(function (e) {
201 | const query = e.target.value.trim();
202 | if (query.length > 0) {
203 | searchTweets(query);
204 | } else {
205 | currentSearchQuery = ''; // Clear search query
206 | fetchAndRenderResults(); // If search box is empty, show all results
207 | }
208 | }, 300)); // 300ms debounce delay
209 | }
210 |
211 | // Function: Fetch data from IndexedDB, sort by time in descending order, and render search results
212 | function fetchAndRenderResults(isInitialLoad = true) {
213 | if (isLoading) return;
214 | isLoading = true;
215 |
216 | getAllTweets()
217 | .then(tweets => {
218 | allTweets = tweets.sort((a, b) => new Date(b.time) - new Date(a.time));
219 | if (currentSearchQuery) {
220 | // If there's a search query, only show filtered results
221 | searchTweets(currentSearchQuery);
222 | } else {
223 | if (isInitialLoad) {
224 | renderSearchResults(allTweets.slice(0, tweetsPerPage));
225 | currentPage = 1;
226 | } else {
227 | const start = currentPage * tweetsPerPage;
228 | const end = start + tweetsPerPage;
229 | const newTweets = allTweets.slice(start, end);
230 | appendSearchResults(newTweets);
231 | currentPage++;
232 | }
233 | }
234 | isLoading = false;
235 | })
236 | .catch(error => {
237 | console.error('Failed to fetch tweets:', error);
238 | isLoading = false;
239 | });
240 | }
241 |
242 | // Debounce function
243 | function debounce(func, delay) {
244 | let debounceTimer;
245 | return function () {
246 | const context = this;
247 | const args = arguments;
248 | clearTimeout(debounceTimer);
249 | debounceTimer = setTimeout(() => func.apply(context, args), delay);
250 | }
251 | }
252 |
253 | function initializeInfiniteScroll() {
254 | window.addEventListener('scroll', () => {
255 | if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 500) {
256 | if (currentSearchQuery) {
257 | // If searching, load more search results
258 | const filteredTweets = allTweets.filter(tweet => {
259 | const lowercaseQuery = currentSearchQuery.toLowerCase();
260 | return (
261 | (tweet.text && tweet.text.toLowerCase().includes(lowercaseQuery)) ||
262 | (tweet.username && tweet.username.toLowerCase().includes(lowercaseQuery)) ||
263 | (tweet.userId && tweet.userId.toLowerCase().includes(lowercaseQuery))
264 | );
265 | });
266 | const start = currentPage * tweetsPerPage;
267 | const end = start + tweetsPerPage;
268 | const newTweets = filteredTweets.slice(start, end);
269 | appendSearchResults(newTweets);
270 | // Apply highlighting to newly loaded results
271 | applyHighlightToResults(currentSearchQuery);
272 | currentPage++;
273 | } else {
274 | // If not searching, load the next page of all tweets
275 | fetchAndRenderResults(false);
276 | }
277 | }
278 | });
279 | }
280 |
281 | // Initialize after page load
282 | document.addEventListener('DOMContentLoaded', () => {
283 | fetchAndRenderResults();
284 | initializeSearch();
285 | initializeInfiniteScroll();
286 | });
--------------------------------------------------------------------------------
/js/service-worker.js:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | chrome.runtime.onInstalled.addListener(() => {
16 | chrome.contextMenus.create({
17 | id: 'openSidePanel',
18 | title: 'Tweet Trace',
19 | contexts: ['all']
20 | });
21 | chrome.tabs.create({ url: 'html/about.html' });
22 | });
23 |
24 | chrome.contextMenus.onClicked.addListener((info, tab) => {
25 | if (info.menuItemId === 'openSidePanel') {
26 | // This will open the panel in all the pages on the current window.
27 | chrome.sidePanel.open({ windowId: tab.windowId });
28 | }
29 | });
30 |
31 | let dbPromise;
32 |
33 | function openIndexedDB() {
34 | if (!dbPromise) {
35 | dbPromise = new Promise((resolve, reject) => {
36 | const request = indexedDB.open('TweetTraceDB', 1);
37 |
38 | request.onupgradeneeded = event => {
39 | const db = event.target.result;
40 | if (!db.objectStoreNames.contains('tweets')) {
41 | db.createObjectStore('tweets', { keyPath: 'id' });
42 | }
43 | };
44 |
45 | request.onsuccess = event => {
46 | resolve(event.target.result);
47 | };
48 |
49 | request.onerror = event => {
50 | reject('IndexedDB error: ' + event.target.errorCode);
51 | };
52 | });
53 | }
54 | return dbPromise;
55 | }
56 |
57 | function storeTweetData(tweetData) {
58 | openIndexedDB().then(db => {
59 | const transaction = db.transaction('tweets', 'readwrite');
60 | const store = transaction.objectStore('tweets');
61 |
62 | // First, get the count of all data
63 | store.count().onsuccess = function(event) {
64 | const count = event.target.result;
65 |
66 | // If the count has reached or exceeded 10000
67 | if (count >= 10000) {
68 | // Get all keys
69 | store.getAllKeys().onsuccess = function(event) {
70 | const keys = event.target.result;
71 | // Delete the oldest data (assuming keys are sorted by insertion order)
72 | store.delete(keys[0]).onsuccess = function() {
73 | // After successful deletion, add new data
74 | store.add(tweetData);
75 | };
76 | };
77 | } else {
78 | // If the count is less than 10000, directly add new data
79 | store.add(tweetData);
80 | }
81 | };
82 |
83 | }).catch(error => {
84 | console.error('Failed to open IndexedDB:', error);
85 | });
86 | }
87 |
88 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
89 | // console.log('Received message:', message);
90 | if (message.type === 'storeTweetData') {
91 | storeTweetData(message.tweetData);
92 | }
93 | });
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "TweetTrace",
4 | "version": "1.0",
5 | "description": "TweetTrace is a free and open-source Chrome extension that records tweets you visit.",
6 | "minimum_chrome_version": "116",
7 | "background": {
8 | "service_worker": "js/service-worker.js"
9 | },
10 | "side_panel": {
11 | "default_path": "html/sidepanel.html"
12 | },
13 | "content_scripts": [
14 | {
15 | "js": [
16 | "js/content-script.js"
17 | ],
18 | "matches": [
19 | "https://x.com/*",
20 | "https://www.twitter.com/*"
21 | ]
22 | }
23 | ],
24 | "permissions": [
25 | "sidePanel",
26 | "contextMenus",
27 | "storage"
28 | ],
29 | "host_permissions": [
30 | "https://x.com/*"
31 | ],
32 | "icons": {
33 | "16": "images/twitter.png",
34 | "48": "images/twitter.png",
35 | "128": "images/twitter.png"
36 | }
37 | }
--------------------------------------------------------------------------------
/privacy-policy.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | At **TweetTrace**, I take your privacy very seriously. This Privacy Policy explains how I collect, use, and protect the personal information you provide to me.
4 |
5 | ## Information I Collect
6 |
7 | 1. I may collect the following information when you use my extension:
8 | - The tweets you browse on Twitter
9 | - The tweet content you save locally on your device
10 |
11 | ## How I Use the Collected Information
12 |
13 | 2. I will use the collected information to:
14 | - Provide you with the ability to search and filter through your recorded tweets
15 | - Locally store your browsing history and saved tweet content on your device
16 | - Improve and optimize the functionality of my extension
17 |
18 | ## Data Protection
19 |
20 | 3. I take the following measures to protect the security of your personal information:
21 | - Store your information in your browser's localStorage, ensuring your data never leaves your device
22 | - I do not analyze, extract, or share your data with any third parties
23 |
24 | If you have any questions or concerns about this Privacy Policy, please feel free to contact me.
25 |
26 | ## Access the Privacy Policy
27 |
28 | You can access this Privacy Policy at: [https://github.com/CstCamaro/TweetTrace/blob/main/privacy-policy.md](https://github.com/CstCamaro/TweetTrace/blob/main/privacy-policy.md)
29 |
--------------------------------------------------------------------------------