mirror of
https://github.com/yusufipk/YouTubeOutlierBadge.git
synced 2026-09-11 10:56:14 +00:00
Outlier score badge on YouTube thumbnails
Scores every card against the median views of its own channel's last N videos, badging the thumbnail above the duration stamp. Shorts and regular videos are scored in separate pools, live streams in none. Baselines come from YouTube's InnerTube endpoint, one request per channel, cached a day. Interface in English and Turkish.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
body {
|
||||
width: 280px;
|
||||
margin: 0;
|
||||
padding: 12px 14px 14px;
|
||||
font: 13px/1.45 "Segoe UI", Roboto, sans-serif;
|
||||
color: #eee;
|
||||
background: #202124;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
select {
|
||||
background: #303134;
|
||||
color: #eee;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #303134;
|
||||
color: #eee;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover { background: #3c4043; }
|
||||
|
||||
.wide { width: 100%; margin-top: 8px; }
|
||||
|
||||
.warn {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: #4a2f12;
|
||||
color: #ffd8a8;
|
||||
}
|
||||
|
||||
.warn button { margin-top: 6px; }
|
||||
|
||||
.stats {
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #3c4043;
|
||||
color: #9aa0a6;
|
||||
font-size: 12px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 10px 0 0;
|
||||
color: #9aa0a6;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Outlier Badge</h1>
|
||||
|
||||
<div id="permission" class="warn" hidden>
|
||||
<span data-i18n="popupPermission"></span>
|
||||
<button id="grant" type="button" data-i18n="popupGrant"></button>
|
||||
</div>
|
||||
|
||||
<label class="row">
|
||||
<span data-i18n="popupEnabled"></span>
|
||||
<input type="checkbox" id="enabled">
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span data-i18n="popupScoreShorts"></span>
|
||||
<input type="checkbox" id="scoreShorts">
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span data-i18n="popupShowPanel"></span>
|
||||
<input type="checkbox" id="showPanel">
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span data-i18n="popupBaselineSize"></span>
|
||||
<select id="baselineSize"></select>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span data-i18n="popupMinScore"></span>
|
||||
<select id="minScore"></select>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span data-i18n="popupTtl"></span>
|
||||
<select id="ttlHours"></select>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span data-i18n="popupLanguage"></span>
|
||||
<select id="language"></select>
|
||||
</label>
|
||||
|
||||
<div class="stats" id="stats"></div>
|
||||
|
||||
<button id="clear" type="button" class="wide" data-i18n="popupClear"></button>
|
||||
|
||||
<p class="note" data-i18n="popupNote"></p>
|
||||
|
||||
<script src="../src/i18n.js"></script>
|
||||
<script src="../src/store.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/* Ayar penceresi. Kaydetmek için düğme yok: her değişiklik anında yazılır,
|
||||
* açık YouTube sekmeleri storage.onChanged ile kendini yeniler. */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var api = (typeof browser !== "undefined" ? browser : chrome);
|
||||
var T = OBI18n;
|
||||
var ORIGINS = { origins: ["*://www.youtube.com/*"] };
|
||||
|
||||
var FIELDS = [
|
||||
["enabled", "checkbox"],
|
||||
["scoreShorts", "checkbox"],
|
||||
["showPanel", "checkbox"],
|
||||
["baselineSize", "number"],
|
||||
["minScore", "number"],
|
||||
["ttlHours", "number"],
|
||||
["language", "text"]
|
||||
];
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function option(select, value, text) {
|
||||
var opt = document.createElement("option");
|
||||
opt.value = String(value);
|
||||
opt.textContent = text;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
|
||||
/* Seçenek metinleri dile göre değiştiği için HTML'de değil burada üretilir. */
|
||||
function buildOptions() {
|
||||
var sizes = $("baselineSize");
|
||||
sizes.textContent = "";
|
||||
[15, 30, 60, 90].forEach(function (n) { option(sizes, n, T.t("optLastN", n)); });
|
||||
|
||||
var scores = $("minScore");
|
||||
scores.textContent = "";
|
||||
option(scores, 0, T.t("optShowAll"));
|
||||
[1, 1.5, 2, 3].forEach(function (n) { option(scores, n, T.num(n, n % 1 ? 1 : 0) + "x"); });
|
||||
|
||||
var ttl = $("ttlHours");
|
||||
ttl.textContent = "";
|
||||
option(ttl, 6, T.t("optHours", 6));
|
||||
option(ttl, 24, T.t("optHours", 24));
|
||||
option(ttl, 72, T.t("optDays", 3));
|
||||
option(ttl, 168, T.t("optWeek"));
|
||||
|
||||
var lang = $("language");
|
||||
lang.textContent = "";
|
||||
option(lang, "auto", T.t("optAuto"));
|
||||
option(lang, "tr", "Türkçe");
|
||||
option(lang, "en", "English");
|
||||
}
|
||||
|
||||
function applyTexts() {
|
||||
var nodes = document.querySelectorAll("[data-i18n]");
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
nodes[i].textContent = T.t(nodes[i].dataset.i18n);
|
||||
}
|
||||
}
|
||||
|
||||
function fillValues(s) {
|
||||
FIELDS.forEach(function (f) {
|
||||
var node = $(f[0]);
|
||||
if (f[1] === "checkbox") node.checked = !!s[f[0]];
|
||||
else node.value = String(s[f[0]]);
|
||||
});
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
return OBStore.settings().then(function (s) {
|
||||
T.init(s.language);
|
||||
applyTexts();
|
||||
buildOptions();
|
||||
fillValues(s);
|
||||
return OBStore.stats();
|
||||
}).then(function (st) {
|
||||
$("stats").textContent = T.t("popupStatsChannels", st.channels);
|
||||
return askTabStats();
|
||||
});
|
||||
}
|
||||
|
||||
/* Açık YouTube sekmesindeki sayaçlar: rozet çıkmıyorsa nedeni burada görünür. */
|
||||
function askTabStats() {
|
||||
return api.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
|
||||
if (!tabs.length) return;
|
||||
return api.tabs.sendMessage(tabs[0].id, { type: "ob-stats" }).then(function (r) {
|
||||
if (!r) return;
|
||||
var c = r.counters;
|
||||
var line = T.t("popupStatsTab", c.seen, c.scored);
|
||||
if (c.skipped) line += T.t("popupStatsSkipped", c.skipped);
|
||||
if (c.failed) line += T.t("popupStatsFailed", c.failed);
|
||||
$("stats").textContent += "\n" + line + ".";
|
||||
if (r.lastError) $("stats").textContent += "\n" + T.t("popupLastError", r.lastError);
|
||||
});
|
||||
}).catch(function () { /* sekmede içerik betiği yok, önemsiz */ });
|
||||
}
|
||||
|
||||
function bind() {
|
||||
FIELDS.forEach(function (f) {
|
||||
$(f[0]).addEventListener("change", function () {
|
||||
var node = $(f[0]);
|
||||
var patch = {};
|
||||
patch[f[0]] = f[1] === "checkbox" ? node.checked
|
||||
: f[1] === "number" ? parseFloat(node.value) : node.value;
|
||||
OBStore.saveSettings(patch).then(function () {
|
||||
if (f[0] === "language") refresh();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$("clear").addEventListener("click", function () {
|
||||
OBStore.clearBaselines().then(function () {
|
||||
$("stats").textContent = T.t("popupCleared");
|
||||
});
|
||||
});
|
||||
|
||||
$("grant").addEventListener("click", function () {
|
||||
api.permissions.request(ORIGINS).then(function (granted) {
|
||||
if (granted) $("permission").hidden = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
api.permissions.contains(ORIGINS).then(function (has) {
|
||||
$("permission").hidden = !!has;
|
||||
}).catch(function () {});
|
||||
|
||||
bind();
|
||||
refresh();
|
||||
})();
|
||||
Reference in New Issue
Block a user