4PDA Fonts & Radio v.1.1

Дополнения для более комфортного пребывания на 4PDA

  1. // ==UserScript==
  2. // @name 4PDA Fonts & Radio v.1.1
  3. // @author brant34
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.1-full
  6. // @description Дополнения для более комфортного пребывания на 4PDA
  7. // @match https://4pda.to/forum/*
  8. // @grant GM_addStyle
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const savedSize = GM_getValue('size', '14px');
  17. const savedFont = GM_getValue('font', 'verdana');
  18. const savedAutoplay = GM_getValue('autoplay', false);
  19. const panelScale = GM_getValue('panelSize', '1');
  20. const panelPosition = GM_getValue('panelPos', 'right');
  21. const savedRadio = GM_getValue('radio', '');
  22. const savedVolume = GM_getValue('volume', 1);
  23. const savedTimer = GM_getValue('autotimer', 0);
  24. const savedPlaying = GM_getValue('isPlaying', false);
  25. const savedTime = GM_getValue('currentTime', 0);
  26.  
  27. const FONTS = {
  28. 'verdana': 'Verdana','georgia': 'Georgia','open-sans': 'Open Sans','comfortaa': 'Comfortaa','nunito': 'Nunito',
  29. 'pt-sans': 'PT Sans','manrope': 'Manrope','rubik': 'Rubik','roboto': 'Roboto','ubuntu': 'Ubuntu','noto-sans': 'Noto Sans','montserrat': 'Montserrat'
  30. };
  31.  
  32. const RADIO = {
  33. '🇷🇺 Европа Плюс': 'https://ep256.hostingradio.ru:8052/europaplus256.mp3',
  34. '🇷🇺 Русское Радио': 'https://rusradio.hostingradio.ru/rusradio128.mp3',
  35. '🇷🇺 Юмор FM': 'https://pub0301.101.ru:8443/stream/air/mp3/256/102',
  36. '🇷🇺 Радио Рекорд': 'https://radio-srv1.11one.ru/record192k.mp3',
  37. '🇷🇺 Ретро FM': 'https://retro.hostingradio.ru:8014/retro320.mp3',
  38. '🇷🇺 Радио Шансон': 'https://chanson.hostingradio.ru:8041/chanson256.mp3',
  39. '🇷🇺 DFM Russian Dance': 'https://stream03.pcradio.ru/dfm_russian_dance-hi',
  40. '🇷🇺 DFM': 'https://dfm.hostingradio.ru:80/dfm96.aacp',
  41. '🇷🇺 Дорожное Радио': 'https://dorognoe.hostingradio.ru:8000/dorognoe',
  42. '🇷🇺 Авторадио': 'https://srv01.gpmradio.ru/stream/air/aac/64/100?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiIwZWM3MjU3YTFhNDM5MmMyNWUwZDZkZDQwYjdjNzQ5ZCIsIklQIjoiODEuMTczLjE2NS4yMjUiLCJVQSI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzMuMC4wLjAgU2FmYXJpLzUzNy4zNiIsIlJlZiI6Imh0dHBzOi8vd3d3LmF2dG9yYWRpby5ydS8iLCJ1aWRfY2hhbm5lbCI6IjEwMCIsInR5cGVfY2hhbm5lbCI6ImNoYW5uZWwiLCJ0eXBlRGV2aWNlIjoiUEMiLCJCcm93c2VyIjoiQ2hyb21lIiwiQnJvd3NlclZlcnNpb24iOiIxMzMuMC4wLjAiLCJTeXN0ZW0iOiJNYWMgT1MgWCBQdW1hIiwiZXhwIjoxNzQyNjcxOTc1fQ.b1Hha0aGp4hWbgFELSzEapRcpOoejzs8tmdDARY0JyA',
  43. '🇩🇪 Радио Картина': 'https://rs.kartina.tv/kartina_320kb',
  44. '🇰🇿 LuxFM': 'https://icecast.luxfm.kz/luxfm',
  45. '🇰🇿 Radio NS': 'https://icecast.ns.kz/radions',
  46. '🇰🇿 NRJ Kazakhstan': 'https://stream03.pcradio.ru/energyfm_ru-med',
  47. '🇰🇿 Радио Жаңа FM': 'https://live.zhanafm.kz:8443/zhanafm_onair',
  48. '🇺🇦 Хіт FM': 'http://online.hitfm.ua/HitFM',
  49. '🇺🇦 Kiss FM UA': 'http://online.kissfm.ua/KissFM'
  50. };
  51.  
  52. GM_addStyle(`
  53. @import url('https://fonts.googleapis.com/css2?family=Manrope&display=swap');
  54. @import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');
  55. @import url('https://fonts.googleapis.com/css2?family=Rubik&display=swap');
  56. @import url('https://fonts.googleapis.com/css2?family=Comfortaa&display=swap');
  57. @import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');
  58. @import url('https://fonts.googleapis.com/css2?family=PT+Sans&display=swap');
  59. @import url('https://fonts.googleapis.com/css2?family=Open+Sans&display=swap');
  60. @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
  61. @import url('https://fonts.googleapis.com/css2?family=Ubuntu&display=swap');
  62. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap');
  63. `);
  64.  
  65. function applyStyles(font, size) {
  66. const selectors = ['body','.post','.msg','.signature','.post_wrap','.xbox','.code','.normalname','.desc','.maintitle','.postcolor','.nav','td','th'];
  67. selectors.forEach(selector => {
  68. document.querySelectorAll(selector).forEach(el => {
  69. el.style.setProperty('font-family', `'${FONTS[font]}', sans-serif`, 'important');
  70. el.style.setProperty('font-size', size, 'important');
  71. });
  72. });
  73. }
  74.  
  75. function createPanelSettings(panel) {
  76. const gear = document.createElement('span');
  77. gear.textContent = '⚙️';
  78. gear.style.cursor = 'pointer';
  79. gear.style.marginLeft = '6px';
  80. gear.title = 'Настройки панели';
  81.  
  82. const settingsPanel = document.createElement('div');
  83. settingsPanel.style = 'background:#003b3b;color:white;padding:6px;border-radius:6px;position:absolute;right:0;top:120%;z-index:10001;display:none;font-size:12px;min-width:150px;box-shadow:0 0 6px black;';
  84. settingsPanel.innerHTML = `
  85. <div style="margin-bottom:6px;">📏 Размер панели:<br>
  86. <select id="panelSize">
  87. <option value="0.8">Small</option>
  88. <option value="1">Medium</option>
  89. <option value="1.3">Large</option>
  90. </select>
  91. </div>
  92. <div>📍 Положение панели:<br>
  93. <select id="panelPos">
  94. <option value="left">Слева</option>
  95. <option value="center">Посередине</option>
  96. <option value="right">Справа</option>
  97. </select>
  98. </div>`;
  99.  
  100. gear.onclick = () => {
  101. settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
  102. };
  103.  
  104. setTimeout(() => {
  105. settingsPanel.querySelector('#panelSize').value = panelScale;
  106. settingsPanel.querySelector('#panelPos').value = panelPosition;
  107. }, 0);
  108.  
  109. settingsPanel.querySelector('#panelSize').onchange = e => {
  110. GM_setValue('panelSize', e.target.value);
  111. panel.style.transform = `scale(${e.target.value})`;
  112. panel.style.transformOrigin = panelPosition === 'left' ? 'top left' : (panelPosition === 'center' ? 'top center' : 'top right');
  113. };
  114.  
  115. settingsPanel.querySelector('#panelPos').onchange = e => {
  116. GM_setValue('panelPos', e.target.value);
  117. const pos = e.target.value;
  118. panel.style.top = '0px'; // Фиксируем под самый верх
  119. panel.style.bottom = 'auto';
  120. panel.style.left = pos === 'left' ? '10px' : (pos === 'center' ? '50%' : 'auto');
  121. panel.style.right = pos === 'right' ? '10px' : 'auto';
  122. panel.style.transformOrigin = pos === 'left' ? 'top left' : (pos === 'center' ? 'top center' : 'top right');
  123. if (pos === 'center') panel.style.transform = `translateX(-50%) scale(${panelScale})`;
  124. else panel.style.transform = `scale(${panelScale})`;
  125. };
  126.  
  127. return { gear, settingsPanel };
  128. }
  129.  
  130. function createPanel() {
  131. const panel = document.createElement('div');
  132. panel.id = 'customFontPanel';
  133. const pos = panelPosition;
  134. panel.style = `
  135. position:fixed;
  136. top:0px; /* Под самый верх */
  137. bottom:auto;
  138. left:${pos === 'left' ? '10px' : (pos === 'center' ? '50%' : 'auto')};
  139. right:${pos === 'right' ? '10px' : 'auto'};
  140. background:#004c4c;
  141. color:white;
  142. padding:10px;
  143. border-radius:10px;
  144. z-index:10000;
  145. font-family:sans-serif;
  146. font-size:14px;
  147. box-shadow:0 0 10px rgba(0,0,0,0.3);
  148. display:none;
  149. min-width:200px;
  150. transform:${pos === 'center' ? `translateX(-50%) scale(${panelScale})` : `scale(${panelScale})`};
  151. transform-origin:${pos === 'left' ? 'top left' : (pos === 'center' ? 'top center' : 'top right')};
  152. `;
  153.  
  154. const title = document.createElement('div');
  155. title.textContent = '⚡ Профили:';
  156. title.style.marginBottom = '4px';
  157. panel.appendChild(title);
  158.  
  159. const profiles = document.createElement('div');
  160. profiles.style.display = 'flex';
  161. profiles.style.flexWrap = 'wrap';
  162. profiles.style.gap = '6px';
  163. ['Минимум','Комфорт','Ночь'].forEach(p => {
  164. const btn = document.createElement('button');
  165. btn.textContent = p;
  166. btn.style.cssText = 'padding: 4px 8px; border-radius: 6px; border: none; cursor: pointer; background: #089; color: #fff;';
  167. btn.onclick = () => {
  168. if (p === 'Минимум') { GM_setValue('font','open-sans'); GM_setValue('size','12px'); }
  169. if (p === 'Комфорт') { GM_setValue('font','manrope'); GM_setValue('size','14px'); }
  170. if (p === 'Ночь') { GM_setValue('font','rubik'); GM_setValue('size','16px'); }
  171. location.reload();
  172. };
  173. profiles.appendChild(btn);
  174. });
  175. panel.appendChild(profiles);
  176.  
  177. const fontSelect = document.createElement('select');
  178. for (const key in FONTS) {
  179. const opt = document.createElement('option');
  180. opt.value = key;
  181. opt.textContent = FONTS[key];
  182. opt.style.fontFamily = FONTS[key];
  183. if (key === savedFont) opt.selected = true;
  184. fontSelect.appendChild(opt);
  185. }
  186. fontSelect.onchange = () => {
  187. GM_setValue('font', fontSelect.value);
  188. applyStyles(fontSelect.value, GM_getValue('size', '14px'));
  189. };
  190.  
  191. const sizeSelect = document.createElement('select');
  192. ['12px','14px','16px','18px','20px'].forEach(px => {
  193. const opt = document.createElement('option');
  194. opt.value = px;
  195. opt.textContent = px;
  196. if (px === savedSize) opt.selected = true;
  197. sizeSelect.appendChild(opt);
  198. });
  199. sizeSelect.onchange = () => {
  200. GM_setValue('size', sizeSelect.value);
  201. applyStyles(GM_getValue('font', 'verdana'), sizeSelect.value);
  202. };
  203.  
  204. const radioSelect = document.createElement('select');
  205. const none = document.createElement('option');
  206. none.textContent = '-- Радио --';
  207. none.value = '';
  208. radioSelect.appendChild(none);
  209. for (const name in RADIO) {
  210. const opt = document.createElement('option');
  211. opt.value = RADIO[name];
  212. opt.textContent = name;
  213. if (RADIO[name] === savedRadio) opt.selected = true;
  214. radioSelect.appendChild(opt);
  215. }
  216.  
  217. const audio = document.createElement('audio');
  218. audio.controls = true;
  219. audio.volume = savedVolume;
  220. audio.style.width = '100%';
  221. if (savedRadio) audio.src = savedRadio;
  222.  
  223. radioSelect.onchange = () => {
  224. GM_setValue('radio', radioSelect.value);
  225. audio.src = radioSelect.value;
  226. audio.play();
  227. GM_setValue('isPlaying', true);
  228. };
  229.  
  230. audio.onvolumechange = () => GM_setValue('volume', audio.volume);
  231.  
  232. audio.ontimeupdate = () => {
  233. GM_setValue('currentTime', audio.currentTime);
  234. };
  235. audio.onplay = () => GM_setValue('isPlaying', true);
  236. audio.onpause = () => GM_setValue('isPlaying', false);
  237.  
  238. if (savedRadio && savedAutoplay) {
  239. setTimeout(() => {
  240. audio.play();
  241. audio.currentTime = savedTime;
  242. }, 1000);
  243. if (savedTimer > 0) setTimeout(() => audio.pause(), savedTimer * 60000);
  244. } else if (savedRadio && savedPlaying) {
  245. setTimeout(() => {
  246. audio.play();
  247. audio.currentTime = savedTime;
  248. }, 1000);
  249. }
  250.  
  251. const timerBox = document.createElement('select');
  252. timerBox.innerHTML = `
  253. <option value="0">⏱ Без таймера</option>
  254. <option value="15">⏱ 15 мин</option>
  255. <option value="30">⏱ 30 мин</option>
  256. <option value="60">⏱ 60 мин</option>
  257. `;
  258. timerBox.value = savedTimer;
  259. timerBox.onchange = () => GM_setValue('autotimer', parseInt(timerBox.value));
  260.  
  261. const autoStart = document.createElement('label');
  262. autoStart.style = 'display:flex;align-items:center;margin-top:5px;gap:4px;position:relative';
  263. const autoCb = document.createElement('input');
  264. autoCb.type = 'checkbox';
  265. autoCb.checked = savedAutoplay;
  266. autoCb.onchange = () => GM_setValue('autoplay', autoCb.checked);
  267. autoStart.appendChild(autoCb);
  268. autoStart.appendChild(document.createTextNode('Автостарт'));
  269.  
  270. const { gear, settingsPanel } = createPanelSettings(panel);
  271. autoStart.appendChild(gear);
  272. autoStart.appendChild(settingsPanel);
  273.  
  274. panel.appendChild(fontSelect);
  275. panel.appendChild(sizeSelect);
  276. panel.appendChild(radioSelect);
  277. panel.appendChild(audio);
  278. panel.appendChild(timerBox);
  279. panel.appendChild(autoStart);
  280. document.body.appendChild(panel);
  281. }
  282.  
  283. function createIconButton() {
  284. const button = document.createElement('div');
  285. button.textContent = 'S';
  286. button.style = 'position:fixed;top:20px;right:20px;width:40px;height:40px;background:#2e7d78;color:#fff;font-weight:bold;font-size:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 0 8px rgba(0,0,0,0.3);z-index:10001';
  287. button.onclick = () => {
  288. const panel = document.getElementById('customFontPanel');
  289. panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  290. };
  291. document.body.appendChild(button);
  292. }
  293.  
  294. applyStyles(savedFont, savedSize);
  295. window.addEventListener('load', () => {
  296. createPanel();
  297. createIconButton();
  298. });
  299. })();
  300.  
  301.  
  302. // ========== RADIO FIX PATCH BEGIN ==========
  303. if (!document.hidden && window === window.top && savedRadio) {
  304. if (!document.getElementById('radioPlayer4PDA')) {
  305. const audio = document.createElement('audio');
  306. audio.id = 'radioPlayer4PDA';
  307. audio.controls = true;
  308. audio.volume = savedVolume;
  309. audio.style = 'position:fixed;bottom:10px;right:10px;z-index:10000;width:250px;box-shadow:0 0 6px #000;border-radius:6px;';
  310. audio.src = savedRadio;
  311. document.body.appendChild(audio);
  312.  
  313. audio.onvolumechange = () => GM_setValue('volume', audio.volume);
  314. audio.ontimeupdate = () => GM_setValue('currentTime', audio.currentTime);
  315. audio.onplay = () => GM_setValue('isPlaying', true);
  316. audio.onpause = () => GM_setValue('isPlaying', false);
  317.  
  318. audio.currentTime = GM_getValue('currentTime', 0);
  319. if (savedAutoplay || GM_getValue('isPlaying', false)) {
  320. const tryPlay = () => {
  321. audio.play().catch(() => setTimeout(tryPlay, 1000));
  322. };
  323. tryPlay();
  324. if (savedTimer > 0) setTimeout(() => audio.pause(), savedTimer * 60000);
  325. }
  326. }
  327. }
  328. // ========== RADIO FIX PATCH END ==========
  329.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址