// ==UserScript==
// @name ইউটিউব বাংলা ভাষাতে হাটি ভিউ এবং লাটি ভিউকে পরিবর্তন করুন
// @namespace https://github.com/tazihad
// @version 1.05
// @description ইউটিউব বাংলা ভাষাতে হাটি ভিউ এবং লাটি ভিউকে পরিবর্তন করুন। যথারিতী হাজার ভিউ এবং লাখ ভিউ তে।
// @author zihad
// @match *://*.youtube.com/*
// @match *://*.youtube.com/watch*
// @noframes
// @exclude *://docs.google.com/*
// @exclude *://drive.google.com/*
// @exclude *://mail.google.com/*
// @require https://code.jquery.com/jquery-3.4.1.min.js
// @license CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/
// @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
// ==/UserScript==
/* jshint esversion: 6 */
function waitForKeyElements (
selectorTxt, /* Required: The jQuery selector string that
specifies the desired element(s).
*/
actionFunction, /* Required: The code to run when elements are
found. It is passed a jNode to the matched
element.
*/
bWaitOnce, /* Optional: If false, will continue to scan for
new elements even after the first match is
found.
*/
iframeSelector /* Optional: If set, identifies the iframe to
search.
*/
) {
var targetNodes, btargetsFound;
if (typeof iframeSelector == "undefined")
targetNodes = $(selectorTxt);
else
targetNodes = $(iframeSelector).contents ()
.find (selectorTxt);
if (targetNodes && targetNodes.length > 0) {
btargetsFound = true;
/*--- Found target node(s). Go through each and act if they
are new.
*/
targetNodes.each ( function () {
var jThis = $(this);
var alreadyFound = jThis.data ('alreadyFound') || false;
if (!alreadyFound) {
//--- Call the payload function.
var cancelFound = actionFunction (jThis);
if (cancelFound)
btargetsFound = false;
else
jThis.data ('alreadyFound', true);
}
} );
}
else {
btargetsFound = false;
}
//--- Get the timer-control variable for this selector.
var controlObj = waitForKeyElements.controlObj || {};
var controlKey = selectorTxt.replace (/[^\w]/g, "_");
var timeControl = controlObj [controlKey];
//--- Now set or clear the timer as appropriate.
if (btargetsFound && bWaitOnce && timeControl) {
//--- The only condition where we need to clear the timer.
clearInterval (timeControl);
delete controlObj [controlKey]
}
else {
//--- Set a timer, if needed.
if ( ! timeControl) {
timeControl = setInterval ( function () {
waitForKeyElements ( selectorTxt,
actionFunction,
bWaitOnce,
iframeSelector
);
},
300
);
controlObj [controlKey] = timeControl;
}
}
waitForKeyElements.controlObj = controlObj;
}
(function () {
'use strict';
if(0){/*
*/}
// ==== AA. initial setup =====================================================================|
const scriptPrefix = "rplt-";
const scriptTag = "RPLT";
let runScript = 1;
runScript = getOptionState("enable-"+ scriptPrefix +"script", runScript);
if (runScript) {
let enableConsoleMessages = 1; // default 0; set to 1 to show console messages.
enableConsoleMessages = getOptionState("log-"+ scriptPrefix +"msg", enableConsoleMessages);
let enabledMessages =
//"MA|"+ // any rule matches
"TT-MA|"+ // page title match.
"TXT-MA|"+ // all text matches.
"DLT1|"+ // delete1 matches.
"DLT2|"+ // delete2 matches
"FR1|"+ // full replace matches.
//"CH-TT|"+ // changed text.
"RUNT|"+ // runtime messages (amount of time to execute)
"EXEC|"+ // execution messages (when code is executed)
"\\bST\\b|"+ //script option change update
"GEN$|"+ // general messages
"^1"; // high priority messages
let logAll = 0; // if 1, logs all titles from blocks.
logAll = getOptionState("log-"+ scriptPrefix +"all", logAll);
if (logAll) {
enabledMessages = enabledMessages.concat("|title");
}
const enabledMessagesRegex = new RegExp(enabledMessages); // used in consolelog().
consolelog("#### ("+ scriptTag +") text replace script began. ####", "EXEC");
// ==== AB. replace rules =================================================================|
let replaceRules = [
//[//i, ""], // rule template
// basic examples:
//[/commit/i, "dog"],
//[/branch/i, "turtle"],
//[/file/i, "birdie"],
//[/\w/g, "a"], //replaces all characters with "a".
//[/(.|\W)+/i, "text"], //replaces all text instances with "text".
[/লাটি ভিউ/i, "লাখ ভিউ"],
[/হাটি ভিউ/i, "হাজার ভিউ"],
[/হা জন/i, "হাজার জন"],
[/কো জন/i, "কোটি জন"],
[/লা জন/i, "লাখ জন"],
];
// ==== AC. special rules =================================================================|
const enableSpecialRules = 1;
if (enableSpecialRules) {
// example of including a rule list defined in a spearate script.
if (unsafeWindow.globalListName) {
replaceRules = replaceRules.concat(unsafeWindow.globalListName);
}
}
//consolelog(replaceRules,"all rules"); //test: double check rule contents
// ==== BA. script options ================================================================|
const classWhitelist = /notif-hidden|notif-text|tag-inst|-counter/i;
// text nodes with parent elements with these classes are excluded.
const generateRecheckButton = 1;
let dynamicChecking = 1; // default 1; set to 1 to run the script automatically when new image elements are detected.
dynamicChecking = getOptionState("enable-"+ scriptPrefix +"dynamic-checking", dynamicChecking);
// setting to 0 would make this run a few more times when dynamically checking.
// ==== checked in processPage() ====
// managable with optional Script Options userscript.
let logRuntimes = 1; // default 0; set to 1 to log function runtimes to the console.
let markCheckedElements = 1; // default 1; set to 0 if certain sites start appearing weirdly.
let enableSpecialReplace = 1;
let fullDelete = 0; // default 1; if 1, text is completely replaced.
let addTag = 1; // if fullDelete is active, adds a tag without replacing.
// ==== BB. notif code ====================================================================|
// 'script options' options
let enableExecCounter = 0;
enableExecCounter = getOptionState("enable-"+ scriptPrefix +"counter", enableExecCounter);
let enableNotifications = 0;
enableNotifications = getOptionState("enable-"+ scriptPrefix +"notifs", enableNotifications);
let autohideNotifs = 0; // default 0; notifs disappear after a set period of time. used in createNotif()
let startCollapsed = 1; //default 1;
// notif css variables.
const notifsHex = "#ddd";
const notifsOpacity = .4; // default .4; set to a value between 0 and 1, 1 is no transparency, .5 is 50% transparency.
const notifsWidth = 120; // default 120; width in pixels of each notification.
let notifContainerId = "notif-main-container";
// generate notif container if needed.
if ((enableExecCounter || enableNotifications) && !jQuery("#"+ notifContainerId).length) {
// ==== setting/checking initial visual state of notifs ====
// constrolled exclusively by local storage or the default value.
const localStorageName = "notif start collapsed";
if (window.localStorage.getItem(localStorageName)) {
startCollapsed = window.localStorage.getItem(localStorageName);
startCollapsed = (startCollapsed == "true");
}
const visibleClass = "notif-visible";
const hiddenClass = "notif-hidden1";
let startingStateClass = visibleClass;
let otherStartingStateClass = hiddenClass;
if (startCollapsed) {
startingStateClass = hiddenClass;
otherStartingStateClass = visibleClass;
}
// ==== create container ==============================================================|
/*
[ notif main container
[notif1] - counters
[hide] - button
[open] - button
[close] - button
[clear] - button
[notif2
[dlt-container]
[ll-container]
[ot-container]
]
]
- hide: makes visible open | hides close, clear, notif2
- open: makes visible hide, close, clear, notif2 | hides open
- close: deletes notif main container.
- clear: empties notif-container2
*/
const openButtonId = "notif-open";
const hideButtonId = "notif-hide";
let notificationsElement =
"<div id='"+ notifContainerId +"'>"+
"<div id='notif-container1'></div>"+
"<div id='"+ hideButtonId +"' class='notif-red notif-rounded-block "+ startingStateClass +"'>notif hide</div>"+
"<div id='"+ openButtonId +"' class='notif-green notif-rounded-block "+ otherStartingStateClass +"'>notif open</div>"+
"<div id='notif-close' class='notif-gray notif-rounded-block "+ startingStateClass +"'>close notif[]</div>"+
"<div id='notif-clear' class='notif-orange notif-rounded-block "+ startingStateClass +"'>clear notif</div>"+
"<div id='notif-container2' class=' "+ startingStateClass +"'>"+
"<div id='dlt-container'></div>"+
"<div id='ll-container' class='notif-hidden1'></div>"+
"<div id='ot-container' class='notif-hidden1'</div>"+
"</div>"+
"</div>";
jQuery("body").prepend(notificationsElement);
let textReaderElement =
"<div id='notif-text-overlay' class='notif-text-hidden'></div>";
jQuery("body").prepend(textReaderElement);
jQuery('#notif-container2').on( {
mouseenter: function () {
let notifText = jQuery(this).find(".notif-text").text();
let notifClassList = this.className;
if (/red/.test(notifClassList)) {
jQuery("#notif-text-overlay").addClass("notif-red");
}else if (/orange/.test(notifClassList)) {
jQuery("#notif-text-overlay").addClass("notif-orange");
}else if (/yellow/.test(notifClassList)) {
jQuery("#notif-text-overlay").addClass("notif-yellow");
}else {
jQuery("#notif-text-overlay").addClass("notif-gray");
}
jQuery("#notif-text-overlay").text(notifText);
jQuery("#notif-text-overlay").addClass("notif-text-visible");
},
mouseleave: function () {
jQuery("#notif-text-overlay").removeClass("notif-text-visible");
jQuery("#notif-text-overlay").removeClass("notif-red");
jQuery("#notif-text-overlay").removeClass("notif-orange");
}
}, '.notif-instance');
// ==== close ====
jQuery("#notif-close").click(function(){
jQuery("#"+notifContainerId).remove();
//console.log("RPL notif close clicked. ("+notifContainerId+")");
});
// ==== clears container2 which contains notif instances. ====
function clearNotif(){
jQuery("#notif-container2").empty();
}
jQuery("#notif-clear").click(clearNotif);
// ==== open/hide events ==============================================================|
const mainSelector = "#notif-container2, #"+ hideButtonId +", #notif-close, #notif-clear";
jQuery("#"+ hideButtonId).click(function () {
//console.log(hideButtonId);
window.localStorage.setItem(localStorageName, true);
switchClasses(
mainSelector,
"#"+ openButtonId,
visibleClass,
hiddenClass
);
});
jQuery("#"+ openButtonId).click(function () {
//console.log(openButtonId);
window.localStorage.setItem(localStorageName, false);
switchClasses(
mainSelector,
"#"+ openButtonId,
hiddenClass,
visibleClass
);
});
function switchClasses(mainSelector, subSelector, removedClass, newClass) {
jQuery(mainSelector).removeClass(removedClass);
jQuery(mainSelector).addClass(newClass);
jQuery(subSelector).removeClass(newClass);
jQuery(subSelector).addClass(removedClass);
}
// ==== CSS ===========================================================================|
if(1){var notifsCss =
`<style type="text/css">
#`+ notifContainerId +` {
width: `+ notifsWidth +`px;
max-height: 50%;
margin: 0 2px 2px;
display: block;
line-height: initial;
color: #000;
opacity: `+ notifsOpacity +`;
position: fixed;
top: 0px;
right: 0px;
z-index: 9999;
overflow-y: auto;
}
#`+ notifContainerId +`:hover {
opacity: 1;
}
.notif-rounded-block {
display: block;
padding: 2px;
border-radius: 3px;
margin-top: 2px;
font-size: 11px !important;
font-weight: bold;
text-align: center;
cursor: pointer;
}
.s-counter {
display: block;
padding: 2px;
border-radius: 4px;
margin-top: 2px;
background: #ddd;
font-size: 11px !important;
font-weight: bold;
text-align: center;
}
.notif-text-hidden {
display:none;
}
.notif-text-visible {
display: block;
max-width: 50%;
padding: 5px;
border: #999 solid 2px;
border-radius: 10px;
position: fixed;
top: 5px;
left: 5px;
z-index: 999999;
font-size: 15px !important;
font-weight: bold !important;
text-align: center !important;
color: black !important;
}
.notif-instance {
display: block;
padding: 2px;
border-radius: 4px;
margin-top: 2px;
background: `+ notifsHex +`;
font-size: 11px !important;
font-weight: bold;
text-align: center;
cursor: pointer;
}
.notif-instance div{/* div holding the rule.*/
max-height: 12px;
padding: 0px;
margin: 0px;
border: 0px;
overflow: hidden;
word-break: break-all;
}
.notif-hidden{ /* meant to hide the rule */
opacity: .1;
}
.notif-hidden:hover {
opacity: 1;
}
.notif-red {
background: #f67066;
}
.notif-orange {
background: #ffc107; //yellowish
}
.notif-yellow {
background: #ffc107; //yellowish
}
.notif-green {
background: #62bb66;
}
.notif-gray {
background: #777;
}
/* collapsible classes */
.notif-hidden1 {
display: none !important;
}
.notif-visible {
display: block !important;
}
div#ll-container, div#ot-container {
border-top: solid black 3px;
}
</style>`;
}
jQuery(document.body).append(notifsCss);
}
if(enableExecCounter) {
jQuery("#notif-container1").prepend("<div id='"+ scriptTag +"-counter' class='s-counter .notif-rounded-block'>T No text nodes found.</div>");
}
// resets lastIndex on tests with global modifiers.
RegExp.prototype.regexTest = function(testString){
//consolelog("## regexTest() ##", 1);
if (this.test(testString)) {
if (/.\/i?g/.test(this) && this.lastIndex) {//regex global modifier needs to be reset.
//consolelog("## last index: "+ this.lastIndex +" ##", 1);
this.lastIndex = 0;
}
return true;
}
return false;
};
NodeList.prototype.forEach = Array.prototype.forEach;
// ==== CA. processPage() =================================================================|
// ==== processPage() globals ====
let titleChecked = 0; // if the page title was checked or not.
let fullCheck = 0;
// ==== counters ====
let nodeCounter = 0; // counts text nodes.
let deleteMatches = 0;
let fullReplaceMatches = 0;
let executionCounter = 0; // the number of times processPage() was executed.
function processPage() {
executionCounter++;
logRuntimes = getOptionState("log-"+ scriptPrefix +"runtimes", logRuntimes);
if (logRuntimes) {
var startTime = performance.now();
}
let rulesNum = replaceRules.length;
// per element variables
let ruleMatched = 0;
// ==== checks the title of the page ==================================================|
if(1){
let titleText = jQuery("title").text();
if (titleText && !titleChecked) {
for (let index = 0; index < rulesNum; index++) {
if (replaceRules[index][0].regexTest(titleText)) {
consolelog(scriptTag +" (title match): "+ titleText +" | "+ replaceRules[index][0], "TT-MA");
titleText = titleText.replace(replaceRules[index][0], replaceRules[index][1]);
jQuery("title").text(titleText);
}
}
titleChecked = 1;
}
}
// ==== selects specified text elements ===============================================|
if(1){
const excludedElements = /CODE|SCRIPT|STYLE|TEXTAREA/i;
const checkClassRegex = new RegExp(scriptPrefix +"node","i");
var textWalker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
if (node.nodeValue.trim() &&
!excludedElements.test(node.parentNode.nodeName) && // exclude scripts and style elements
(fullCheck || !checkClassRegex.test(node.parentNode.classList)) && // exclude checked elements
!classWhitelist.test(node.parentNode.classList)) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
}
},
false
);
}
let textNode = textWalker.nextNode();
let previousParent;
// ==== for each textNode =============================================================|
while (textNode) {
let nodeText = textNode.nodeValue; // is changed based on matches.
if (!fullCheck) {
let immediateParentNode = textNode.parentNode; // element containing the text node.
nodeCounter++;
// common case: new parent is different than previous.
if (previousParent && !(previousParent == immediateParentNode) ) {
markCheckedElements = getOptionState(scriptPrefix +"mark-checked", markCheckedElements);
if (markCheckedElements) {
// only sets checked class if previous parent is different.
previousParent.classList.add(scriptPrefix +"node-"+ nodeCounter);
}
previousParent = immediateParentNode;
}else if (!previousParent) {
previousParent = immediateParentNode;
}
}
// ==== for each rule =============================================================|
for (let index = 0; index < rulesNum; index++) {
let currentRuleRegex = replaceRules[index][0];
let replacementValue = replaceRules[index][1];
if (currentRuleRegex.regexTest(nodeText.trim())) {
ruleMatched = 1;
let matchPrefix = "GEN0";
consolelog("("+ scriptTag +") (n)"+ nodeCounter +" (match): "+ nodeText.trim() +" | "+ currentRuleRegex, "TXT-MA");
const disableReplace = 0; // test: check what is checked through each run.
if (!disableReplace) {
enableSpecialReplace = getOptionState("enable-special-replace", enableSpecialReplace);
// ==== delete1 match =================================================|
if (enableSpecialReplace && (/DELETE1/.test(replacementValue) || /DELETE2/.test(replacementValue)) ) {
deleteMatches++;
matchPrefix = "DLT99";
if (/DELETE1/.test(replacementValue) && !/DELETE1/.test(nodeText)) {
matchPrefix = "DLT1";
}else if (/DELETE2/.test(replacementValue) && !/DELETE2/.test(nodeText)) {
matchPrefix = "DLT2";
}
consolelog("("+ scriptTag +") ("+ matchPrefix +") n"+ nodeCounter +" (match): "+ nodeText.trim() +" | "+ currentRuleRegex, matchPrefix);
createNotif(nodeCounter +" "+ matchPrefix, currentRuleRegex, nodeText);
fullDelete = getOptionState("enable-full-delete", fullDelete);
addTag = getOptionState("add-tag", addTag);
const tagRegex = new RegExp("^\\["+matchPrefix);
if (fullDelete) {
nodeText = "## "+ matchPrefix +" ##"; // replaces the text completely.
break;
}else if (addTag && !tagRegex.text(nodeText)) {
nodeText = "["+ matchPrefix +"]: " + nodeText; // prepends DLT1 or DLT2
}
}
// ==== full replace match ============================================|
if (enableSpecialReplace && /^FR1/.test(replacementValue)) {
fullReplaceMatches++;
matchPrefix = "FR1";
consolelog("("+ scriptTag +") ("+ matchPrefix +") n"+ nodeCounter +" (match): "+ nodeText.trim() +" | "+ currentRuleRegex, matchPrefix);
createNotif(nodeCounter +" "+ matchPrefix, currentRuleRegex, nodeText);
nodeText = replacementValue;
break;
}
// ==== base case =====================================================|
nodeText = nodeText.replace(currentRuleRegex, replacementValue);
} // end if (!disableReplace)
}
} // end for (each rule) ==========================================================|
if (ruleMatched) { // modify text block.
ruleMatched = 0;
textNode.nodeValue = nodeText;
consolelog("("+ scriptTag +") (n)"+ nodeCounter +" (text): "+ nodeText.trim(), "CH-TT");
}
textNode = textWalker.nextNode();
} // end while (textNode) =============================================================|
if (!fullCheck) {
// ==== update counter ====
let counterText = "T DLT:"+ deleteMatches +" | FR:"+ fullReplaceMatches +" | N:"+ nodeCounter + " | EX:"+ executionCounter;
jQuery("#"+ scriptTag +"-counter").text(counterText);
if (nodeCounter) {
jQuery("#"+ scriptTag +"-counter").addClass("notif-green");
}
}else { //end fullCheck.
fullCheck = 0;
}
//consolelog("## ("+ scriptTag +") execution #"+ executionCounter +" ##", "EXEC");
// script option handles if this is displayed or not.
if (logRuntimes) {
const endTime = performance.now();
const runTime = ((endTime - startTime) / 1000).toFixed(2);
if (runTime > 1) {
consolelog('('+ scriptTag +') finished after ' + runTime + ' seconds.', "RUNT");
}else {
consolelog('('+ scriptTag +') finished in less than 1 second.', "RUNT");
}
}
} //end function function replaceText()
// ==== CB. execution control =============================================================|
//console.log("("+ scriptTag +") EXEC: Initial run.");
//processPage();
let runWhenReady = 0;
runWhenReady = getOptionState("run-when-ready", runWhenReady);
if (runWhenReady) {
jQuery(document).ready(function() { //after DOM has loaded.
consolelog("("+ scriptTag +") EXEC: document.ready()", "EXEC");
//fullCheck = 1;
processPage();
});
}
let runWhenLoaded = 1;
runWhenLoaded = getOptionState("run-when-loaded", runWhenLoaded);
if (runWhenLoaded) {
jQuery(window).on("load", function() { //after all initial images are loaded.
consolelog("("+ scriptTag +") EXEC: window.load()", "EXEC");
//fullCheck = 1;
processPage();
});
}
if (dynamicChecking) {
jQuery(document).ready(waitForKeyElements("img", processPage));
}
// ==== DA. script button =================================================================|
let buttonsContainerId = "ctb-container1";
if (generateRecheckButton && jQuery("#"+ buttonsContainerId).length) {
jQuery("#"+ buttonsContainerId).prepend("<div id='"+ scriptTag +"-reset' class='ctb-blue ctb-rounded-block'>run "+ scriptTag +"</div>"); //added to beginning
//jQuery("#"+ scriptTag +"-reset").click(processPage);
jQuery("#"+ scriptTag +"-reset").click(function() {
fullCheck = 1;
processPage();
});
}
// ==== DB. support functions =============================================================|
function createNotif(notifLabel, notifRule, notifText) { //msg1 needs to match notifTypes
enableNotifications = getOptionState("enable-"+ scriptPrefix +"notifs", enableNotifications);
if (enableNotifications) {
let additionalClass = "notif-gray";
let notifContainer = "ot-container";
if (/dlt/i.test(notifLabel)) {
additionalClass = "notif-red";
notifContainer = "dlt-container";
}
let newNotif =
"<div class='notif-instance "+ additionalClass +"'><div>t n"+ notifLabel +"</div>"+
"<div class='notif-hidden'>"+ notifRule +"</div>"+
"<div class='notif-text' hidden>"+ notifText+"</div>"+ // to be displayed at the bottom left.
"</div>";
let enabledNotifTypesRegex = /./;
if (enabledNotifTypesRegex.test(notifLabel)) {
jQuery("#"+ notifContainer).append(newNotif);
jQuery(".notif-instance").click(function(){
jQuery("#notif-container2").empty();
});
if (!/dlt/i.test(notifLabel)) {
jQuery("#ot-container").removeClass("notif-hidden1");
}
autohideNotifs = getOptionState("autohide-notifications", autohideNotifs);
if (autohideNotifs) {
const notifDuration = 10; // default 10; amount of seconds notifications are displayed before disappearing.
setTimeout(function() {
jQuery(".notif-instance").remove();
}, notifDuration*1000);
}
}
}
} // end function creatNotif()
function consolelog(text, messageType) {
if (enableConsoleMessages && enabledMessagesRegex.test(messageType)) {
console.log(text);
}
}
// ==== script end ========================================================================|
consolelog("#### ("+ scriptTag +") text replace script is active. ####", "EXEC");
} // end if (runScript)
// ============================================================================================|
// = getOptionState(, );
// used to update option if 'script option' is set.
function getOptionState(idName, currentState) {
if (document.getElementById(idName)) {
return document.getElementById(idName).checked;
}
return currentState;
}
})();