Skip to content

Instantly share code, notes, and snippets.

@alankyshum
Last active March 20, 2022 23:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alankyshum/4dbe38f9caabfc7d3784ad9fb012ef8f to your computer and use it in GitHub Desktop.
Save alankyshum/4dbe38f9caabfc7d3784ad9fb012ef8f to your computer and use it in GitHub Desktop.
My own custom tampermonkey scripts
/**
* Wait for the element to appear in the DOM within specified timeout
* @param {string} selector
* @param {number} timeout
* @returns {Promise<HTMLElement[]>}
*/
async function getElements(selector = "", timeout = 20000) {
let intervalClearer;
let timeRemaining = timeout;
return new Promise((resolve, reject) => {
intervalClearer = setInterval(() => {
const queriedElements = document.querySelectorAll(selector);
const isElementsLoaded = /[A-Z]/.test(queriedElements[0]?.textContent);
if (timeRemaining < 0) {
clearInterval(intervalClearer);
console.error(new Error(`${selector} was not found`));
return;
} else if (isElementsLoaded) {
clearInterval(intervalClearer);
resolve(queriedElements);
return;
}
timeRemaining -= 500;
}, 500);
});
}
// ==UserScript==
// @name Finviz - Screener to map
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Render a button linking to a maps view of currently viewing stocks from screener page
// @author Alan Shum
// @match *://elite.finviz.com/screener.ashx*
// @icon https://www.google.com/s2/favicons?sz=64&domain=finviz.com
// @grant none
// ==/UserScript==
(function () {
"use strict";
class FinvizExtended {
/** @type {HTMLStyleElement} */
styleElement;
/** @type {HTMLDivElement} */
wrapperElement;
constructor() {
this.wrapperElement = document.createElement('div');
this.wrapperElement.id = "ky-script--wrapper";
this.styleElement = document.createElement('style');
this.styleElement.textContent = `
#ky-script--wrapper {
top: 16px;
right: 8px;
position: fixed;
z-index: 99999;
}
.ky-script--extension-button {
background: var(--button-color);
color: white;
box-shadow: 0px 0px 10px var(--button-color);
opacity: 0.7;
padding: 4px 16px;
border: 2px solid white;
border-radius: 4px;
margin: 16px;
transition: opacity .3s;
}
.ky-script--extension-button:hover,
.ky-script--extension-button:focus {
opacity: 1;
}
`;
this.wrapperElement.appendChild(this.styleElement);
}
injectScreenerButton() {
this._injectStyle`
.ky-script--screener-button {
--button-color: black;
}
`;
const allTickers = Array.from(document.querySelectorAll('a[href^="quote.ashx"]'))
.map((a) => new URL(a.href).searchParams.get('t'));
const tickers = Array.from(new Set(allTickers)).join(',');
const screenerUrl = `https://elite.finviz.com/bubbles.ashx?x=PE&y=operMargin&size=relativeVolume&color=sector&idx=any&tickers=${tickers}`;
const screenerAnchor = document.createElement('a');
screenerAnchor.setAttribute('href', screenerUrl);
screenerAnchor.setAttribute('target', '_blank');
screenerAnchor.textContent = `View in map ➾`;
screenerAnchor.classList.add('ky-script--screener-button');
screenerAnchor.classList.add('ky-script--extension-button');
this.wrapperElement.appendChild(screenerAnchor);
}
_injectStyle(cssStr) {
this.styleElement.textContent += cssStr;
}
}
/* ========================================================================== */
/* MAIN */
/* ========================================================================== */
const finvizExtended = new FinvizExtended();
finvizExtended.injectScreenerButton();
document.body.appendChild(finvizExtended.wrapperElement);
})();
// ==UserScript==
// @name Robinhood - Portfolio in Finviz
// @namespace http://tampermonkey.net/
// @version 0.2
// @description View portfolio in Finviz
// @author Alan Shum
// @match https://robinhood.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=robinhood.com
// @grant none
// ==/UserScript==
(async function () {
"use strict";
/**
* Main class for all the extended features for Finviz
*/
class FinvizExtended {
/**
* Containing all features overlaying the Robinhood webpage
*/
overflowWrapper = new OverflowWrapper();
/**
* Inject Finviz chart underneath the Robinhood chart
*/
async injectFinvizChart() {
const [parentContainer] = await getElements('#sdp-price-chart-graph');
if (!parentContainer) return;
const currentTicker = location.pathname.match(/\/stocks\/(?<ticker>[A-Z]+)/i)?.groups?.ticker;
if (!currentTicker) return;
const yearlyChartImage = document.createElement('img');
yearlyChartImage.src = `https://charts2.finviz.com/chart.ashx?ty=c&s=l&p=d&ta=1&t=${currentTicker}`;
yearlyChartImage.style.width = '100%';
const linkWrapper = document.createElement('a');
linkWrapper.setAttribute('target', '_blank');
linkWrapper.href = `https://elite.finviz.com/quote.ashx?t=${currentTicker}`;
linkWrapper.appendChild(yearlyChartImage);
parentContainer.appendChild(linkWrapper);
}
async injectViewChartsButton() {
await this.overflowWrapper.appendStyle`
.ky-script--screener-button {
--button-color: black;
}
`;
const targetTickers = await getElements('[data-testid="PositionCell"]');
const allTickers = Array.from(targetTickers)
.map(pos => pos.textContent.match(/[A-Z]+/)[0]);
const tickers = Array.from(new Set(allTickers)).join(',');
const screenerUrl = `https://elite.finviz.com/screener.ashx?v=211&t=${tickers}&o=-relativevolume`;
const screenerAnchor = document.createElement('a');
screenerAnchor.setAttribute('href', screenerUrl);
screenerAnchor.setAttribute('target', '_blank');
screenerAnchor.textContent = "View Finviz Charts ➾";
screenerAnchor.classList.add('ky-script--screener-button');
screenerAnchor.classList.add('ky-script--extension-button');
this.overflowWrapper.appendChild(screenerAnchor);
}
}
/* ========================================================================== */
/* BUTTONS OVERLAYING THE WEBPAGE */
/* ========================================================================== */
/**
* Elements overlay the whole webpage will be inserted into this wrapper.
*/
class OverflowWrapper {
/** @type {HTMLDivElement} */
wrapperElement;
WRAPPER_ID = 'ky-script--wrapper';
get styleElement() {
return this.wrapperElement?.querySelector('style');
}
/**
* Append an element to the overflow wrapper, and init wrapper if not present
* @param {HTMLElement} child
*/
async appendChild(child) {
if (!this.wrapperElement) {
await this.initOverflowWrapper();
}
this.wrapperElement.appendChild(child);
}
/**
* Create the wrapper element and append it to the body
*/
async initOverflowWrapper() {
this.wrapperElement = document.createElement('div');
this.wrapperElement.id = this.WRAPPER_ID;
const styleElement = document.createElement('style');
styleElement.textContent = `
#${this.WRAPPER_ID} {
top: 64px;
right: 8px;
position: fixed;
z-index: 99999;
}
.ky-script--extension-button {
background: var(--button-color);
color: white;
box-shadow: 0px 0px 10px var(--button-color);
opacity: 0.7;
padding: 4px 16px;
border: 2px solid white;
border-radius: 4px;
margin: 16px;
text-decoration: none;
transition: opacity .3s;
}
.ky-script--extension-button:hover,
.ky-script--extension-button:focus {
opacity: 1;
}
`;
this.wrapperElement.appendChild(styleElement);
document.body.appendChild(this.wrapperElement);
}
/**
* Append styles class into the style element inside the overflow wrapper
* @param {string} cssString
*/
async appendStyle(cssString) {
if (!this.wrapperElement) {
await this.initOverflowWrapper();
}
this.styleElement.textContent += cssString;
}
}
/* ========================================================================== */
/* UTILITY METHODS */
/* ========================================================================== */
/**
* Wait for the element to appear in the DOM within specified timeout
* @param {string} selector
* @param {number} timeout
* @returns {HTMLElement[]}
*/
async function getElements(selector = '', timeout = 20000) {
let intervalClearer;
let timeRemaining = timeout;
return new Promise((resolve, reject) => {
intervalClearer = setInterval(() => {
const queriedElements = document.querySelectorAll(selector);
const isElementsLoaded = /[A-Z]/.test(queriedElements[0]?.textContent);
if (timeRemaining < 0) {
clearInterval(intervalClearer);
console.error(new Error(`${selector} was not found`));
return;
} else if (isElementsLoaded) {
clearInterval(intervalClearer);
resolve(queriedElements);
return;
}
timeRemaining -= 500;
}, 500);
});
}
/* ========================================================================== */
/* MAIN */
/* ========================================================================== */
const finvizExtended = new FinvizExtended();
finvizExtended.injectViewChartsButton();
finvizExtended.injectFinvizChart();
})();
// ==UserScript==
// @name name-of-the-script
// @namespace http://tampermonkey.net/
// @description Adds 2 custom search filters to limit search results to the past 6 months or 2 years.
// @contributor Alan Shum
// @version 0.1
// @icon http://www.google.com/favicon.ico
// @include /(http|https)?://.*\.google\.[^\/]+?/(#.*|search\?.*)?$/
// @grant none
// ==/UserScript==

2-step Setup

  1. Install Tampermonkey
  2. View "Raw" to install the script
// ==UserScript==
// @name Ticktick - Matrix as homepage
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Show the Ticktick Eisenhower Matrix as homepage
// @author Alan Shum
// @match https://ticktick.com/webapp/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=ticktick.com
// @grant none
// ==/UserScript==
(async function () {
"use strict";
class ClockWidget {
clock = (() => {
const spanElement = document.createElement("span");
spanElement.style.marginLeft = "4px";
return spanElement;
})();
_isClockInjected = false;
get _currentTimeStamp() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
}
get _workFocusRemaining() {
return this._remainingFocusTime([9, 0], [17, 0]);
}
get _personalFocusRemaining() {
return this._remainingFocusTime([17, 0], [22, 30]);
}
get _formattedContent() {
return `
${this._currentTimeStamp} | 🤖: ${this._workFocusRemaining} | 🤪: ${this._personalFocusRemaining}
`;
}
async initClockWidget() {
this.clockContainer = (await getElements("#container-main h1"))[0];
this.clockContainer.style.display = "flex";
if (!this.clockContainer) return;
this.clockContainer.appendChild(this.clock);
this._isClockInjected = true;
}
renderClock() {
if (!this._isClockInjected) return;
this.clock.innerHTML = this._formattedContent;
return setInterval(this.renderClock.bind(this), 1000);
}
_formatTimestamp(timeInMs) {
const timeInS = timeInMs / 1000;
const hours = Math.floor(timeInS / 3600);
const minutes = Math.floor((timeInS % 3600) / 60);
const seconds = Math.floor((timeInS % 3600) % 60);
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(
2,
"0"
)}:${String(seconds).padStart(2, "0")}`;
}
_remainingFocusTime(focusStartHours, focusEndHours) {
const now = new Date();
const focusTimeStart = new Date().setHours(...focusStartHours);
const focusTimeEnd = new Date().setHours(...focusEndHours);
if (now < focusTimeStart) return "-";
return this._formatTimestamp(focusTimeEnd - now);
}
}
/**
* Wait for the element to appear in the DOM within specified timeout
* @param {string} selector
* @param {number} timeout
* @returns {Promise<HTMLElement[]>}
*/
async function getElements(selector = "", timeout = 20000) {
let intervalClearer;
let timeRemaining = timeout;
return new Promise((resolve, reject) => {
intervalClearer = setInterval(() => {
const queriedElements = document.querySelectorAll(selector);
const isElementsLoaded = /[A-Z]/.test(queriedElements[0]?.textContent);
if (timeRemaining < 0) {
clearInterval(intervalClearer);
console.error(new Error(`${selector} was not found`));
return;
} else if (isElementsLoaded) {
clearInterval(intervalClearer);
resolve(queriedElements);
return;
}
timeRemaining -= 500;
}, 500);
});
}
const clockWidget = new ClockWidget();
await clockWidget.initClockWidget();
clockWidget.renderClock();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment