// ==UserScript==
// @name nodeLoc Level
// @version 0.0.1
// @description Enhanced script to track progress towards next trust level on nodeloc.cc with added search functionality, adjusted posts read limit, and a breathing icon animation.
// @author Alterem
// @match https://nodeloc.cc/*
// @icon https://www.google.com/s2/favicons?domain=nodeloc.cc
// @grant none
// @license MIT
// @namespace https://greasyfork.org/users/1467173
// @downloadURL https://update.greasyfork.icu/scripts/535305/nodeLoc%20Level.user.js
// @updateURL https://update.greasyfork.icu/scripts/535305/nodeLoc%20Level.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONSTANTS = {
BREATH_ANIMATION_DURATION: 4,
MINIMIZED_WIDTH: '60px',
MINIMIZED_HEIGHT: '50px',
NORMAL_WIDTH: '280px',
NORMAL_HEIGHT: 'auto',
DEFAULT_POPUP_BOTTOM: '20px',
DEFAULT_POPUP_RIGHT: '20px',
ACCENT_COLOR: '#9AC5AA'
};
const StyleManager = {
styles: `
@keyframes breathAnimation {
0%, 100% { transform: scale(1); box-shadow: 0 0 5px ${CONSTANTS.ACCENT_COLOR}; }
50% { transform: scale(1.1); box-shadow: 0 0 10px ${CONSTANTS.ACCENT_COLOR}; }
}
.breath-animation { animation: breathAnimation ${CONSTANTS.BREATH_ANIMATION_DURATION}s ease-in-out infinite; }
.minimized { border-radius: 50%; cursor: pointer; }
.nodeLocLevelPopup { position: fixed; width: ${CONSTANTS.NORMAL_WIDTH}; height: ${CONSTANTS.NORMAL_HEIGHT}; background: var(--d-sidebar-background); box-shadow: 0 0 10px rgba(0,0,0,0.5); padding: 15px; z-index: 10000; font-size: 14px; border-radius: 5px; cursor: move; }
.nodeLocLevelPopup input, .nodeLocLevelPopup button { width: 100%; margin-top: 10px; }
.nodeLocLevelPopup button {
cursor: pointer;
background-color: ${CONSTANTS.ACCENT_COLOR};
color: #fff;
border: none;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.3s ease;
}
.nodeLocLevelPopup button:hover {
background-color: #7bb398;
}
.minimizeButton { position: absolute; top: 5px; right: 5px; background: transparent; border: none; cursor: pointer; width: 30px; height: 30px; font-size: 16px; }
.searchButton { width: 100%; marginTop: 10px }
.searchBox { width: 100%; marginTop: 10px }
.minimized-width { width: ${CONSTANTS.MINIMIZED_WIDTH} !important; }
.minimized-height { height: ${CONSTANTS.MINIMIZED_HEIGHT} !important; }
.hidden { display: none !important; }
.loading { text-align: center; padding: 10px; }
.error { color: red; padding: 10px; }
`,
injectStyles: function() {
const styleSheet = document.createElement('style');
styleSheet.type = 'text/css';
styleSheet.innerText = this.styles;
document.head.appendChild(styleSheet);
}
};
const DataManager = {
Config: {
BASE_URL: 'https://nodeloc.cc',
PATHS: {
ABOUT: '/about.json',
USER_SUMMARY: '/u/{username}/summary.json',
USER_DETAIL: '/u/{username}.json',
},
},
cache: new Map(),
levelRequirements: {
0: { 'topics_entered': 5, 'posts_read_count': 30, 'time_read': 600 },
1: { 'days_visited': 15, 'likes_given': 1, 'likes_received': 1, 'post_count': 3, 'topics_entered': 20, 'posts_read_count': 100, 'time_read': 3600 },
2: { 'days_visited': 50, 'likes_given': 30, 'likes_received': 20, 'post_count': 10 },
},
levelDescriptions: {
0: "白银会员",
1: "黄金会员",
2: "钻石会员",
3: "王者会员",
4: "遥不可及"
},
async fetch(url, options = {}) {
if (this.cache.has(url)) {
const {data, timestamp} = this.cache.get(url);
if (Date.now() - timestamp < 600000) {
return data;
}
}
try {
const response = await fetch(url, {
...options,
headers: {
"Accept": "application/json",
"User-Agent": "Mozilla/5.0"
},
method: options.method || "GET"
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
this.cache.set(url, {
data,
timestamp: Date.now()
});
return data;
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
throw error;
}
},
fetchAboutData: function() {
const url = this.buildUrl(this.Config.PATHS.ABOUT);
return this.fetch(url);
},
fetchSummaryData: function(username) {
const url = this.buildUrl(this.Config.PATHS.USER_SUMMARY, { username });
return this.fetch(url);
},
fetchUserData: function(username) {
const url = this.buildUrl(this.Config.PATHS.USER_DETAIL, { username });
return this.fetch(url);
},
buildUrl: function(path, params = {}) {
let url = this.Config.BASE_URL + path;
Object.keys(params).forEach(key => {
url = url.replace(`{${key}}`, encodeURIComponent(params[key]));
});
return url;
},
};
const UIManager = {
initPopup: function() {
this.popup = this.createElement('div', { id: 'nodeLocLevelPopup', class: 'nodeLocLevelPopup' });
this.content = this.createElement('div', { id: 'nodeLocLevelPopupContent' }, '欢迎使用 nodeLoc 等级增强插件');
this.searchBox = this.createElement('input', { placeholder: '请输入用户名...', type: 'text', class: 'searchBox' });
this.searchButton = this.createElement('button', { class: 'searchButton' }, '搜索');
this.minimizeButton = this.createElement('button', { }, '隐藏');
this.popup.style.bottom = CONSTANTS.DEFAULT_POPUP_BOTTOM;
this.popup.style.right = CONSTANTS.DEFAULT_POPUP_RIGHT;
this.searchButton.classList.add('btn', 'btn-icon-text', 'btn-default');
this.minimizeButton.classList.add('btn', 'btn-icon-text', 'btn-default');
this.popup.append(this.content, this.searchBox, this.searchButton, this.minimizeButton);
document.body.appendChild(this.popup);
this.minimizeButton.addEventListener('click', () => this.togglePopupSize());
this.searchButton.addEventListener('click', () => EventHandler.debouncedHandleSearch());
this.searchBox.addEventListener('keypress', (event) => {
if (event.key === 'Enter' && !this.popup.classList.contains('minimized')) {
EventHandler.debouncedHandleSearch();
}
});
this.autoFillUsername();
},
autoFillUsername: function() {
const checkUser = () => {
try {
if (typeof Discourse !== 'undefined' &&
Discourse.User &&
Discourse.User.current()) {
const user = Discourse.User.current();
if (user && user.username) {
this.searchBox.value = user.username;
return true;
}
}
return false;
} catch (error) {
console.error("Error getting username:", error);
return false;
}
};
if (checkUser()) return;
let attempts = 0;
const maxAttempts = 10;
const interval = setInterval(() => {
attempts++;
if (checkUser() || attempts >= maxAttempts) {
clearInterval(interval);
}
}, 1000);
},
createElement: function(tag, attributes, text) {
const element = document.createElement(tag);
for (const attr in attributes) {
if (attr === 'class') {
element.classList.add(attributes[attr]);
} else {
element.setAttribute(attr, attributes[attr]);
}
}
if (text) element.textContent = text;
return element;
},
updatePopupContent: function(userSummary, user, userDetail, status) {
if (!userSummary || !user || !userDetail) return;
let content = `信任等级:${DataManager.levelDescriptions[user.trust_level]}
`;
const requirements = DataManager.levelRequirements[user.trust_level] || {};
if (userDetail.invited_by) {
content += `邀请人:${userDetail.invited_by.username}
`;
} else {
content += `邀请人:无
`;
}
content += `最近活跃:${formatTimestamp(userDetail.last_seen_at)}
升级进度:
`;
if (user.trust_level === 2) {
requirements['posts_read_count'] = Math.min(parseInt(parseInt(status.posts_30_days) / 4), 20000);
requirements['topics_entered'] = Math.min(parseInt(parseInt(status.topics_30_days) / 4), 500);
}
if (user.trust_level === 3) {
content += '联系管理员进行py交易以升级到领导者
';
} else if (user.trust_level === 4) {
content += '您已是最高信任等级
';
} else {
let summary = summaryRequired(requirements, userSummary, this.translateStat.bind(this));
content += summary;
}
this.content.innerHTML = content;
},
async togglePopupSize() {
this.popup.classList.toggle('minimized');
this.popup.classList.toggle('breath-animation');
this.popup.classList.toggle('minimized-width');
this.popup.classList.toggle('minimized-height');
this.content.classList.toggle('hidden');
this.searchBox.classList.toggle('hidden');
this.searchButton.classList.toggle('hidden');
this.minimizeButton.textContent = this.popup.classList.contains('minimized') ? '展开' : '隐藏';
// 如果是展开状态且有用户名,自动搜索
if (!this.popup.classList.contains('minimized') && this.searchBox.value.trim()) {
await EventHandler.handleSearch();
}
enableDraggable(this.popup);
this.correctPopupPosition();
},
correctPopupPosition: function() {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const popupWidth = this.popup.offsetWidth;
const popupHeight = this.popup.offsetHeight;
const popupTop = parseInt(this.popup.style.top);
const popupLeft = parseInt(this.popup.style.left);
let newTop = popupTop;
let newLeft = popupLeft;
newTop = Math.min(Math.max(70, popupTop), windowHeight - popupHeight);
newLeft = Math.min(Math.max(5, popupLeft), windowWidth - popupWidth - 20);
this.popup.style.top = newTop + 'px';
this.popup.style.left = newLeft + 'px';
},
displayError: function(message) {
this.content.innerHTML = `