您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Library for Toggl-Button scripts used on platforms like drupal, github, youtrack and others.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/2670/229280/TogglLibrary.js
/*------------------------------------------------------------------------ * JavaScript Library for Toggl-Button for GreaseMonkey * * (c) Jürgen Haas, PARAGON Executive Services GmbH * Version: 2.0 * * @see https://gitlab.lakedrops.com/toggl-button/core *------------------------------------------------------------------------ */ function TogglButtonGM(selector, renderer) { var $activeApiUrl = null, $apiUrl = "https://www.toggl.com/api/v7", $newApiUrl = "https://www.toggl.com/api/v8", $legacyApiUrl = "https://new.toggl.com/api/v8", $triedAlternative = false, $addedDynamicListener = false, $api_token = null, $default_wid = null, $clientMap = {}, $projectMap = {}, $taskMap = {}, $instances = {}; init(selector, renderer); function init(selector, renderer, apiUrl) { var timeNow = new Date().getTime(), timeAuth = GM.getValue('_authenticated', 0), body = document.querySelector('body'); // Build recurring tasks panel if (!body.classList.contains('toggl-inited')) { body.classList.add('toggl-inited'); var mainContainer = document.createElement('div'), contentContainer = document.createElement('div'), logoContainer = document.createElement('div'); mainContainer.id = 'toggl-wrapper'; mainContainer.classList.add('collapsed'); contentContainer.classList.add('content'); logoContainer.classList.add('logo'); mainContainer.appendChild(logoContainer); mainContainer.appendChild(contentContainer); logoContainer.addEventListener('click', function (e) { e.preventDefault(); mainContainer.classList.toggle('collapsed'); }); body.appendChild(mainContainer); } apiUrl = apiUrl || $newApiUrl; $api_token = GM.getValue('_api_token', false); if ($api_token && (timeNow - timeAuth) < (6*60*60*1000)) { $activeApiUrl = GM.getValue('_api_url', $newApiUrl); $default_wid = GM.getValue('_default_wid', 0); $clientMap = JSON.parse(GM.getValue('_clientMap', '{}')); $projectMap = JSON.parse(GM.getValue('_projectMap', '{}')); $taskMap = JSON.parse(GM.getValue('_taskMap', '{}')); if ($activeApiUrl == $legacyApiUrl) { // See issue #22. $activeApiUrl = $newApiUrl; GM.setValue('_api_url', $activeApiUrl); } render(selector, renderer); return; } var headers = {}; if ($api_token) { headers = { "Authorization": "Basic " + btoa($api_token + ':api_token') }; } $activeApiUrl = apiUrl; GM.xmlHttpRequest({ method: "GET", url: apiUrl + "/me?with_related_data=true", headers: headers, onload: function(result) { if (result.status === 200) { var resp = JSON.parse(result.responseText); $clientMap[0] = 'No Client'; if (resp.data.clients) { resp.data.clients.forEach(function (client) { $clientMap[client.id] = client.name; }); } if (resp.data.projects) { resp.data.projects.forEach(function (project) { if ($clientMap[project.cid] == undefined) { project.cid = 0; } if (project.active) { $projectMap[project.id] = { id: project.id, cid: project.cid, name: project.name, billable: project.billable }; } }); } if (resp.data.tasks) { resp.data.tasks.forEach(function (task) { if ($projectMap[task.pid] == undefined) { task.pid = 0; task.cid = 0; } else { task.cid = $projectMap[task.pid]['cid']; } if (task.active) { if ($taskMap[task.cid] == undefined) { $taskMap[task.cid] = {}; } if ($taskMap[task.cid][task.pid] == undefined) { $taskMap[task.cid][task.pid] = {}; } $taskMap[task.cid][task.pid][task.id] = { id: task.id, pid: task.pid, cid: task.cid, name: task.name }; } }); } GM.setValue('_authenticated', new Date().getTime()); GM.setValue('_api_token', resp.data.api_token); GM.setValue('_api_url', $activeApiUrl); GM.setValue('_default_wid', resp.data.default_wid); GM.setValue('_clientMap', JSON.stringify($clientMap)); GM.setValue('_projectMap', JSON.stringify($projectMap)); GM.setValue('_taskMap', JSON.stringify($taskMap)); $api_token = resp.data.api_token; $default_wid = resp.data.default_wid; render(selector, renderer); } else if (!$triedAlternative) { $triedAlternative = true; if (apiUrl === $apiUrl) { init(selector, renderer, $newApiUrl); } else if (apiUrl === $newApiUrl) { init(selector, renderer, $apiUrl); } } else if ($api_token) { // Delete the API token and try again GM.setValue('_api_token', false); $triedAlternative = false; init(selector, renderer, $newApiUrl); } else { var wrapper = document.createElement('div'), content = createTag('div', 'content'), link = createLink('login', 'a', 'https://new.toggl.com/', 'Login'); GM.addStyle(GM.getResourceText('togglStyle')); link.setAttribute('target', '_blank'); wrapper.setAttribute('id', 'toggl-button-auth-failed'); content.appendChild(document.createTextNode('Authorization to your Toggl account failed!')); content.appendChild(link); wrapper.appendChild(content); document.querySelector('body').appendChild(wrapper); } } }); } function render(selector, renderer) { if (selector == null) { return; } var i, len, elems = document.querySelectorAll(selector + ':not(.toggl)'); for (i = 0, len = elems.length; i < len; i += 1) { elems[i].classList.add('toggl'); $instances[i] = new TogglButtonGMInstance(renderer(elems[i])); } if (!$addedDynamicListener) { $addedDynamicListener = true; document.addEventListener('TogglButtonGMUpdateStatus', function() { GM.xmlHttpRequest({ method: "GET", url: $activeApiUrl + "/time_entries/current", headers: { "Authorization": "Basic " + btoa($api_token + ':api_token') }, onload: function (result) { if (result.status === 200) { var resp = JSON.parse(result.responseText), data = resp.data || false; for (i in $instances) { $instances[i].checkCurrentLinkStatus(data); } } } }); }); window.addEventListener('focus', function() { document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus')); }); if (selector !== 'body') { document.body.addEventListener('DOMSubtreeModified', function () { setTimeout(function () { render(selector, renderer); }, 1000); }); } } } this.clickLinks = function() { for (i in $instances) { $instances[i].clickLink(); } }; this.getCurrentTimeEntry = function(callback) { GM.xmlHttpRequest({ method: "GET", url: $activeApiUrl + "/time_entries/current", headers: { "Authorization": "Basic " + btoa($api_token + ':api_token') }, onload: function (result) { if (result.status === 200) { var resp = JSON.parse(result.responseText), data = resp.data || false; if (data) { callback(data.id, true); } } } }); }; this.stopTimeEntry = function(entryId, asCallback) { if (entryId == null) { if (asCallback) { return; } this.getCurrentTimeEntry(this.stopTimeEntry); return; } GM.xmlHttpRequest({ method: "PUT", url: $activeApiUrl + "/time_entries/" + entryId + "/stop", headers: { "Authorization": "Basic " + btoa($api_token + ':api_token') }, onload: function () { document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus')); } }); }; function TogglButtonGMInstance(params) { var $curEntryId = null, $isStarted = false, $link = null, $generalInfo = null, $buttonTypeMinimal = false, $recurringMode = false, $projectSelector = window.location.host, $projectId = null, $projectSelected = false, $projectSelectElem = null, $stopCallback = null, $tags = params.tags || []; this.checkCurrentLinkStatus = function (data) { var started, updateRequired = false; if (!data) { if ($isStarted) { updateRequired = true; started = false; } } else { if ($generalInfo != null) { if (!$isStarted || ($curEntryId != null && $curEntryId != data.id)) { $curEntryId = data.id; $isStarted = false; } } if ($curEntryId == data.id) { if (!$isStarted) { updateRequired = true; started = true; } } else { if ($isStarted) { updateRequired = true; started = false; } } } if (updateRequired) { if (!started) { $curEntryId = null; } if ($link != null) { updateLink(started); } if ($generalInfo != null) { if (data) { var projectName = 'No project', clientName = 'No client'; if (data.pid !== undefined) { if ($projectMap[data.pid] == undefined) { GM.setValue('_authenticated', 0); window.location.reload(); return; } projectName = $projectMap[data.pid].name; clientName = $clientMap[$projectMap[data.pid].cid]; } var content = createTag('div', 'content'), contentClient = createTag('div', 'client'), contentProject = createTag('div', 'project'), contentDescription = createTag('div', 'description'); contentClient.innerHTML = clientName; contentProject.innerHTML = projectName; contentDescription.innerHTML = data.description; content.appendChild(contentClient); content.appendChild(contentProject); content.appendChild(contentDescription); while ($generalInfo.firstChild) { $generalInfo.removeChild($generalInfo.firstChild); } $generalInfo.appendChild(content); } updateGeneralInfo(started); } } }; this.clickLink = function (data) { $link.dispatchEvent(new CustomEvent('click')); }; createTimerLink(params); function createTimerLink(params) { GM.addStyle(GM.getResourceText('togglStyle')); var linkWrapper = document.querySelector('#toggl-wrapper .content'); if (params.recurringMode !== undefined) { $recurringMode = params.recurringMode; } // Build recurring tasks panel if (!linkWrapper.classList.contains('toggl-recurring')) { linkWrapper.classList.add('toggl-recurring'); var $generalTaskInfo = document.createElement('div'), reload = document.createElement('span'); reload.classList.add('reload'); $generalTaskInfo.id = 'toggl-button-recurring-wrapper'; $generalTaskInfo.classList.add('collapsed'); $generalTaskInfo.appendChild(reload); $generalTaskInfo.addEventListener('click', function (e) { e.preventDefault(); $generalTaskInfo.classList.toggle('collapsed'); }); reload.addEventListener('click', function (e) { e.preventDefault(); GM.setValue('_authenticated', 0); window.location.reload(); }); if (Object.keys($taskMap).length > 0) { var list = document.createElement('ul'); for (cid in $taskMap) { var client = document.createElement('li'), projects = document.createElement('ul'); client.innerText = $clientMap[cid]; client.appendChild(projects); list.appendChild(client); for (pid in $taskMap[cid]) { var project = document.createElement('li'), tasks = document.createElement('ul'); project.innerText = $projectMap[pid].name; project.appendChild(tasks); projects.appendChild(project); for (tid in $taskMap[cid][pid]) { var task = $taskMap[cid][pid][tid], container = document.createElement('li'), placeholder1 = document.createElement('span'), placeholder2 = document.createElement('span'); placeholder1.classList.add('action'); placeholder2.classList.add('label'); placeholder2.innerText = task['name']; container.classList.add('task'); container.setAttribute('pid', task['pid']); container.appendChild(placeholder1); container.appendChild(placeholder2); tasks.appendChild(container); } } } $generalTaskInfo.appendChild(list); } linkWrapper.appendChild($generalTaskInfo); } if (params.generalMode !== undefined && params.generalMode) { $generalInfo = document.createElement('div'); $generalInfo.id = 'toggl-button-gi-wrapper'; $generalInfo.addEventListener('click', function (e) { e.preventDefault(); $generalInfo.classList.toggle('collapsed'); }); linkWrapper.appendChild($generalInfo); document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus')); return; } if (params.projectIds !== undefined) { $projectSelector += '-' + params.projectIds.join('-'); } if (params.stopCallback !== undefined) { $stopCallback = params.stopCallback; } updateProjectId(); $link = createLink('toggl-button'); $link.classList.add(params.className); if (params.buttonType === 'minimal') { $link.classList.add('min'); $link.removeChild($link.firstChild); $buttonTypeMinimal = true; } $link.addEventListener('click', function (e) { var opts = ''; e.preventDefault(); if ($isStarted) { stopTimeEntry(); } else { var billable = false; if ($projectId != undefined && $projectId > 0) { billable = $projectMap[$projectId].billable; } opts = { $projectId: $projectId || null, billable: billable, description: invokeIfFunction(params.description), createdWith: 'TogglButtonGM - ' + params.className }; createTimeEntry(opts); } return false; }); // new button created - reset state $isStarted = false; // check if our link is the current time entry and set the state if it is checkCurrentTimeEntry({ $projectId: $projectId, description: invokeIfFunction(params.description) }); document.querySelector('body').classList.add('toggl-button-available'); if (params.targetSelectors == undefined) { var wrapper, existingWrapper = document.querySelectorAll('#toggl-button-wrapper'), content = createTag('div', 'content'); content.appendChild($link); content.appendChild(createProjectSelect()); if (existingWrapper.length > 0) { wrapper = existingWrapper[0]; while (wrapper.firstChild) { wrapper.removeChild(wrapper.firstChild); } wrapper.appendChild(content); } else { wrapper = document.createElement('div'); wrapper.id = 'toggl-button-wrapper'; wrapper.appendChild(content); linkWrapper.appendChild(wrapper); } } else { if (params.targetSelectors.pid != undefined) { $projectId = params.targetSelectors.pid; } var elem = params.targetSelectors.context || document; if (params.targetSelectors.link != undefined) { elem.querySelector(params.targetSelectors.link).appendChild($link); } if (params.targetSelectors.projectSelect != undefined) { elem.querySelector(params.targetSelectors.projectSelect).appendChild(createProjectSelect()); } } return $link; } function createTimeEntry(timeEntry) { var start = new Date(); GM.xmlHttpRequest({ method: "POST", url: $activeApiUrl + "/time_entries", headers: { "Authorization": "Basic " + btoa($api_token + ':api_token') }, data: JSON.stringify({ time_entry: { start: start.toISOString(), description: timeEntry.description, wid: $default_wid, pid: timeEntry.$projectId || null, billable: timeEntry.billable || false, duration: -(start.getTime() / 1000), tags: $tags, created_with: timeEntry.createdWith || 'TogglButtonGM' } }), onload: function (res) { var responseData, entryId; responseData = JSON.parse(res.responseText); entryId = responseData && responseData.data && responseData.data.id; $curEntryId = entryId; document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus')); } }); } function checkCurrentTimeEntry(params) { GM.xmlHttpRequest({ method: "GET", url: $activeApiUrl + "/time_entries/current", headers: { "Authorization": "Basic " + btoa($api_token + ':api_token') }, onload: function (result) { if (result.status === 200) { var resp = JSON.parse(result.responseText); if (resp == null) { return; } if (params.description === resp.data.description) { $curEntryId = resp.data.id; updateLink(true); } } } }); } function stopTimeEntry(entryId) { entryId = entryId || $curEntryId; if (!entryId) { return; } GM.xmlHttpRequest({ method: "PUT", url: $activeApiUrl + "/time_entries/" + entryId + "/stop", headers: { "Authorization": "Basic " + btoa($api_token + ':api_token') }, onload: function (result) { $curEntryId = null; document.dispatchEvent(new CustomEvent('TogglButtonGMUpdateStatus')); if (result.status === 200) { var resp = JSON.parse(result.responseText), data = resp.data || false; if (data) { if ($stopCallback !== undefined && $stopCallback !== null) { var currentdate = new Date(); $stopCallback((currentdate.getTime() - (data.duration * 1000)), data.duration); } } } } }); } function createTag(name, className, innerHTML) { var tag = document.createElement(name); tag.className = className; if (innerHTML) { tag.innerHTML = innerHTML; } return tag; } function createLink(className, tagName, linkHref, linkText) { // Param defaults tagName = tagName || 'a'; linkHref = linkHref || '#'; linkText = linkText || 'Start timer'; var link = createTag(tagName, className); if (tagName === 'a') { link.setAttribute('href', linkHref); } link.appendChild(document.createTextNode(linkText)); return link; } function updateGeneralInfo(started) { if (started) { $generalInfo.classList.add('active'); } else { $generalInfo.classList.remove('active'); } $isStarted = started; } function updateLink(started) { if ($recurringMode) { return; } var linkText, color = ''; if (started) { document.querySelector('body').classList.add('toggl-button-active'); $link.classList.add('active'); color = '#1ab351'; linkText = 'Stop timer'; } else { document.querySelector('body').classList.remove('toggl-button-active'); $link.classList.remove('active'); linkText = 'Start timer'; } $isStarted = started; $link.setAttribute('style', 'color:'+color+';'); if (!$buttonTypeMinimal) { $link.innerHTML = linkText; } $projectSelectElem.disabled = $isStarted; } function updateProjectId(id) { id = id || GM.getValue($projectSelector, 0); $projectSelected = (id != 0); if (id <= 0) { $projectId = null; } else { $projectId = id; } if ($projectSelectElem != undefined) { $projectSelectElem.value = id; $projectSelectElem.disabled = $isStarted; } GM.setValue($projectSelector, id); if ($link != undefined) { if ($projectSelected) { $link.classList.remove('hidden'); } else { $link.classList.add('hidden'); } } } function invokeIfFunction(trial) { if (trial instanceof Function) { return trial(); } return trial; } function createProjectSelect() { var pid, wrapper = createTag('div', 'toggl-button-project-select'), noneOptionAdded = false, noneOption = document.createElement('option'), emptyOption = document.createElement('option'), resetOption = document.createElement('option'); $projectSelectElem = createTag('select'); // None Option to indicate that a project should be selected first if (!$projectSelected) { noneOption.setAttribute('value', '0'); noneOption.text = '- First select a project -'; $projectSelectElem.appendChild(noneOption); noneOptionAdded = true; } // Empty Option for tasks with no project emptyOption.setAttribute('value', '-1'); emptyOption.text = 'No Project'; $projectSelectElem.appendChild(emptyOption); var optgroup, project, clientMap = []; for (pid in $projectMap) { //noinspection JSUnfilteredForInLoop project = $projectMap[pid]; if (clientMap[project.cid] == undefined) { optgroup = createTag('optgroup'); optgroup.label = $clientMap[project.cid]; clientMap[project.cid] = optgroup; $projectSelectElem.appendChild(optgroup); } else { optgroup = clientMap[project.cid]; } var option = document.createElement('option'); option.setAttribute('value', project.id); option.text = project.name; optgroup.appendChild(option); } // Reset Option to reload settings and projects from Toggl resetOption.setAttribute('value', 'RESET'); resetOption.text = 'Reload settings'; $projectSelectElem.appendChild(resetOption); $projectSelectElem.addEventListener('change', function () { if ($projectSelectElem.value == 'RESET') { GM.setValue('_authenticated', 0); window.location.reload(); return; } if (noneOptionAdded) { $projectSelectElem.removeChild(noneOption); noneOptionAdded = false; } updateProjectId($projectSelectElem.value); }); updateProjectId($projectId); wrapper.appendChild($projectSelectElem); return wrapper; } } }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址