├── .gitignore ├── .github └── funding.yml ├── src ├── icon-128.png ├── icon-16.png ├── icon-48.png ├── background.js ├── manifest.json ├── index.css └── content.js ├── screenshots └── twitter-print-styles-v2-example.png ├── .editorconfig ├── .stylelintrc ├── .eslintrc ├── readme.md ├── package.json ├── gulpfile.js └── changelog.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | build/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: tannerhodges 2 | -------------------------------------------------------------------------------- /src/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tannerhodges/twitter-print-styles/HEAD/src/icon-128.png -------------------------------------------------------------------------------- /src/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tannerhodges/twitter-print-styles/HEAD/src/icon-16.png -------------------------------------------------------------------------------- /src/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tannerhodges/twitter-print-styles/HEAD/src/icon-48.png -------------------------------------------------------------------------------- /screenshots/twitter-print-styles-v2-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tannerhodges/twitter-print-styles/HEAD/screenshots/twitter-print-styles-v2-example.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Defaults 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Markdown 13 | [*.md] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // When a user clicks on the extension action... 2 | chrome.action.onClicked.addListener(() => { 3 | // Tell the active tab to load the entire thread. 4 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 5 | const activeTab = tabs[0]; 6 | chrome.tabs.sendMessage(activeTab.id, { message: 'load_entire_thread' }); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "stylelint-order" 4 | ], 5 | "extends": [ 6 | "stylelint-config-standard", 7 | "stylelint-config-property-sort-order-smacss", 8 | ], 9 | "rules": { 10 | "comment-empty-line-before": null, 11 | "max-empty-lines": 3, 12 | "no-descending-specificity": null, 13 | "no-duplicate-selectors": null 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:cypress/recommended", 4 | "eslint-config-airbnb-base" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "es6": true 9 | }, 10 | "globals": { 11 | "chrome" 12 | }, 13 | "rules": { 14 | "arrow-body-style": "off", 15 | "class-methods-use-this": "off", 16 | "no-alert": "off", 17 | "no-await-in-loop": "off", 18 | "no-console": "off", 19 | "no-loop-func": "off", 20 | "no-param-reassign": "off", 21 | "no-return-assign": "off", 22 | "no-underscore-dangle": "off", 23 | "object-curly-newline": "off", 24 | "prefer-destructuring": "off", 25 | "prefer-template": "off" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Twitter Print Styles", 4 | "version": "3.0.0", 5 | "description": "Simple print styles for saving Twitter threads as PDFs.", 6 | "icons": { 7 | "16": "icon-16.png", 8 | "48": "icon-48.png", 9 | "128": "icon-128.png" 10 | }, 11 | "background": { 12 | "service_worker": "background.js" 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "https://twitter.com/*", 18 | "https://x.com/*" 19 | ], 20 | "css": ["index.css"], 21 | "js": ["content.js"] 22 | } 23 | ], 24 | "action": { 25 | "default_icon": "icon-128.png" 26 | }, 27 | "host_permissions": [ 28 | "https://twitter.com/*", 29 | "https://x.com/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 🐦 Twitter Print Styles 2 | 3 | Chrome extension for adding simple print styles to save Twitter threads as PDFs. 4 | 5 | 👉 [Download Chrome Extension](https://chrome.google.com/webstore/detail/twitter-print-styles/bepilablapiogeghmjiopiaoikgdcgjo). 6 | 7 | 👉 [Download Firefox Extension](https://addons.mozilla.org/addon/twitter-print-styles/). 8 | 9 | ![](./screenshots/twitter-print-styles-v2-example.png) 10 | 11 | ## ⭐️ Features 12 | 13 | - Default print styles (print-friendly colors, expand images, hide extra elements). 14 | - Link the first tweet's timestamp to the current URL (so you don't lose it in the PDF). 15 | - Click icon to load entire thread and print. 16 | - Click icon again to stop current task. 17 | 18 | ## 🤷‍♂️ Caveats 19 | 20 | - Does not support modals/popups. 21 | - Does not follow "Show this thread" links. 22 | - Only loads inline replies, not "More Tweets" or "Show more replies" at the very end of a thread. 23 | 24 | ## 👀 Alternatives 25 | 26 | - [JikJi for Twitter Threads](https://jikji.pro) 27 | - [Thread Reader](https://threadreaderapp.com) 28 | 29 | ## 📝 [Changelog](changelog.md) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-print-styles", 3 | "version": "3.0.0", 4 | "description": "Chrome extension for adding simple print styles to save Twitter threads as PDFs.", 5 | "keywords": [ 6 | "chrome", 7 | "extension", 8 | "twitter", 9 | "x", 10 | "print", 11 | "css" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/tannerhodges/twitter-print-styles.git" 16 | }, 17 | "private": false, 18 | "scripts": { 19 | "build": "gulp" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/tannerhodges/twitter-print-styles/issues" 23 | }, 24 | "homepage": "https://github.com/tannerhodges/twitter-print-styles#readme", 25 | "dependencies": { 26 | "eslint": "^7.32.0", 27 | "eslint-config-airbnb-base": "^14.2.1", 28 | "eslint-plugin-import": "^2.24.2", 29 | "gulp": "^4.0.2", 30 | "gulp-postcss": "^9.0.1", 31 | "gulp-replace": "^1.1.3", 32 | "gulp-zip": "^5.1.0", 33 | "postcss": "^8.3.11", 34 | "postcss-clean": "^1.2.2", 35 | "postcss-prefix-selector": "^1.13.0", 36 | "stylelint": "^13.13.1", 37 | "stylelint-config-property-sort-order-smacss": "^7.1.0", 38 | "stylelint-config-standard": "^22.0.0", 39 | "stylelint-order": "^4.1.0" 40 | }, 41 | "author": "Tanner Hodges ", 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { dest, parallel, series, src, watch } = require('gulp'); 2 | const clean = require('postcss-clean'); 3 | const fs = require('fs'); 4 | const postcss = require('gulp-postcss'); 5 | const prefixer = require('postcss-prefix-selector'); 6 | const replace = require('gulp-replace'); 7 | const zip = require('gulp-zip'); 8 | 9 | /** 10 | * Process CSS to work with both default Twitter print styles, 11 | * and the custom print screen that we open in a new window. 12 | */ 13 | function copy() { 14 | return src('src/*') 15 | .pipe(dest('dist/')); 16 | } 17 | exports.copy = copy; 18 | 19 | /** 20 | * Process CSS to work with both default Twitter print styles, 21 | * and the custom print screen that we open in a new window. 22 | */ 23 | const css = parallel( 24 | // Wrap everything in a print media query so we can apply 25 | // it to Twitter's default print styles. 26 | function defaultPrintCSS() { 27 | return src('src/index.css') 28 | .pipe(replace('/* BEGIN */', '@media print {')) 29 | .pipe(replace('/* END */', '}')) 30 | .pipe(dest('dist/')); 31 | }, 32 | // Prefix everything with a custom ID, then compress it to 33 | // fit in a 333 | 334 | 335 | 336 | ${clone.outerHTML} 337 | 338 | `; 339 | 340 | const winUrl = URL.createObjectURL(new Blob([winHtml], { type: 'text/html' })); 341 | 342 | const win = window.open(winUrl); 343 | 344 | // Wait one more time, just to be sure _all_ our media is ready. 345 | await mediaLoaded(win.document.querySelector('body')); 346 | 347 | // Annnd print! 348 | win.print(); 349 | }); 350 | --------------------------------------------------------------------------------