您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Start Toggl Timer when viewing a Favro ticket
当前为
// ==UserScript== // @name Favro - Toggl Timer // @namespace https://www.gotom.io/ // @version 0.7 // @license MIT // @author Mike Meier // @match https://favro.com/* // @match https://www.toggl.com/api/* // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @require http://code.jquery.com/jquery-3.4.1.min.js // @description Start Toggl Timer when viewing a Favro ticket // ==/UserScript== (function ($) { const FAVRO_EMAIL_KEY_NAME = 'favro_email'; const FAVRO_API_KEY_NAME = 'favro_api_key'; const FAVRO_TICKET_PREFIX_KEY_NAME = 'favro_ticket_prefix'; const FAVRO_ORGANIZATION_ID_KEY_NAME = 'favro_organization_id'; const FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME = 'favro_pid_custom_field_id'; const TOGGL_API_KEY_NAME = 'toggl_api_key'; const TOGGL_DEFAULT_PID_KEY_NAME = 'toggl_default_pid'; const ENV_VALUES = [ FAVRO_EMAIL_KEY_NAME, FAVRO_API_KEY_NAME, FAVRO_TICKET_PREFIX_KEY_NAME, FAVRO_ORGANIZATION_ID_KEY_NAME, FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME, TOGGL_API_KEY_NAME, TOGGL_DEFAULT_PID_KEY_NAME ]; const TIMEOUT_BEFORE_TRACKING = 5000; function start() { $.noConflict(); ensureEnvironmentVariables(); window.setInterval(detectOpenCardChanges(onOpenCardChange($)), 1000); window.onbeforeunload = stopTimeEntry; } function ensureEnvironmentVariables() { ENV_VALUES.forEach(async key => { await GM.getValue(key) || GM.setValue(key, prompt(key)); }); } function detectOpenCardChanges(onChange) { let currentOpenCardId = null; return async () => { const ticketPrefix = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME); const regex = new RegExp('\\?card=' + ticketPrefix + '([\\d]+)', 'i'); let search = new URL(location.href).search; let matches = regex.exec(search); let openCard = matches === null ? null : parseInt(matches[1]); if (openCard !== currentOpenCardId) { const oldValue = currentOpenCardId; currentOpenCardId = openCard; onChange(oldValue, openCard); } } } function beforeSendFavro(favroToken, email, organizationId) { return (xhr) => { xhr.setRequestHeader('Authorization', 'Basic ' + btoa(email + ':' + favroToken)); xhr.setRequestHeader('organizationId', organizationId); xhr.setRequestHeader('Content-Type', 'application/json'); } } function getTogglHeaders(togglToken) { return { 'Authorization': 'Basic ' + btoa(togglToken + ':api_token'), 'Content-Type': 'application/json' }; } function getTogglPid(customFields, pidCustomFieldId) { let pid = null; customFields.forEach(customField => { if (customField.customFieldId === pidCustomFieldId) { pid = customField.total; return true; } }); return pid; } let timeEntryId = null; let timeEntryTimoutId = null; function startTimeEntry(card) { stopTimeEntry(); timeEntryTimoutId = window.setTimeout(async () => { const description = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME) + card.sequentialId + ' / ' + card.name + ' (Auto-Toggl)'; const togglToken = await GM.getValue(TOGGL_API_KEY_NAME); const pidCustomFieldId = await GM.getValue(FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME); let pid = getTogglPid(card.customFields, pidCustomFieldId); if (!pid) { pid = await GM.getValue(TOGGL_DEFAULT_PID_KEY_NAME); } const data = JSON.stringify({time_entry: {pid: pid, description: description, created_with: 'tampermonkey'}}); GM.xmlHttpRequest({ method: 'POST', url: 'https://www.toggl.com/api/v8/time_entries/start', data: data, headers: getTogglHeaders(togglToken), onload: (res) => { timeEntryId = JSON.parse(res.response).data.id; const togglButtonId = 'button_' + timeEntryId; const togglButton = $('<button id="' + togglButtonId + '" type="button" style="border:none;background:none;cursor:pointer;">' + '<img src="https://web-assets.toggl.com/app/assets/images/favicon.b87d0d2d.ico" style="width:20px;height:20px;"></button>'); togglButton.click(() => { stopTimeEntry(); $('#' + togglButtonId).remove(); }); $('#' + card.cardId + '.cardeditor').find('.cardeditor-topbar .buttons').append(togglButton); } }); }, TIMEOUT_BEFORE_TRACKING); } async function stopTimeEntry() { if (timeEntryTimoutId) { window.clearTimeout(timeEntryTimoutId); } if (!timeEntryId) { return; } const currentTimeEntryId = timeEntryId; timeEntryId = null; const togglToken = await GM.getValue(TOGGL_API_KEY_NAME); GM.xmlHttpRequest({ method: 'PUT', url: 'https://www.toggl.com/api/v8/time_entries/' + currentTimeEntryId + '/stop', headers: getTogglHeaders(togglToken), }); } function onOpenCardChange($) { return async (oldCard, newCard) => { if (!newCard) { await stopTimeEntry(); return; } const sequentialId = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME) + newCard; const favroToken = await GM.getValue(FAVRO_API_KEY_NAME); const email = await GM.getValue(FAVRO_EMAIL_KEY_NAME); const organizationId = await GM.getValue(FAVRO_ORGANIZATION_ID_KEY_NAME); $.ajax({ type: 'GET', url: 'https://favro.com/api/v1/cards?cardSequentialId=' + sequentialId, beforeSend: beforeSendFavro(favroToken, email, organizationId), success: (res) => { const card = res.entities[0]; if (!card) { console.error('No card found in favro for sequentialId' + sequentialId); return; } startTimeEntry(card); } }); }; } start(); })(window.jQuery);