4PDA Radio v1.14

Радио на 4PDA с поиском через API и флагами стран

  1. // ==UserScript==
  2. // @name 4PDA Radio v1.14
  3. // @author brant34
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.14
  6. // @description Радио на 4PDA с поиском через API и флагами стран
  7. // @match https://4pda.to/forum/*
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_addStyle
  11. // @grant GM_xmlhttpRequest
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // Проверка поддержки localStorage
  18. if (!window.localStorage) {
  19. showNotification('localStorage недоступен', 'error');
  20. return;
  21. }
  22.  
  23. // === [Стили] ===
  24. GM_addStyle(`
  25. .radio-toggle-button {
  26. position: fixed;
  27. top: 10px;
  28. right: 10px;
  29. background-color: #2e6d5e;
  30. color: #fff;
  31. border: none;
  32. border-radius: 50%;
  33. width: 32px;
  34. height: 32px;
  35. cursor: pointer;
  36. font-size: 16px;
  37. line-height: 32px;
  38. text-align: center;
  39. z-index: 99999;
  40. }
  41. .radio-toggle-button:hover {
  42. background-color: #3e8e77;
  43. }
  44. .radio-panel {
  45. display: none;
  46. background-color: #1a3c34;
  47. border-radius: 10px;
  48. padding: 10px;
  49. z-index: 99998;
  50. color: #fff;
  51. font-family: Arial, sans-serif;
  52. font-size: 14px;
  53. width: 320px;
  54. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
  55. position: fixed;
  56. max-width: 90vw;
  57. }
  58. .radio-panel-header {
  59. display: flex;
  60. justify-content: space-between;
  61. align-items: center;
  62. margin-bottom: 10px;
  63. }
  64. .radio-panel-header span {
  65. font-weight: bold;
  66. }
  67. .radio-panel-controls {
  68. display: flex;
  69. flex-wrap: wrap;
  70. gap: 5px;
  71. }
  72. .radio-panel-controls button {
  73. background-color: #17a2b8;
  74. color: #fff;
  75. border: none;
  76. border-radius: 5px;
  77. padding: 5px 10px;
  78. cursor: pointer;
  79. font-size: 12px;
  80. }
  81. .radio-panel-controls button:hover {
  82. background-color: #138496;
  83. }
  84. .radio-panel select {
  85. background-color: #fff;
  86. border: 1px solid #ccc;
  87. border-radius: 5px;
  88. padding: 5px;
  89. color: #333;
  90. font-size: 12px;
  91. flex-grow: 1;
  92. }
  93. .radio-player {
  94. background-color: transparent;
  95. border-radius: 5px;
  96. padding: 5px;
  97. margin: 10px 0;
  98. display: flex;
  99. align-items: center;
  100. gap: 5px;
  101. }
  102. .radio-player button {
  103. background: none;
  104. border: none;
  105. cursor: pointer;
  106. font-size: 16px;
  107. color: #fff;
  108. }
  109. .radio-player input[type="range"] {
  110. -webkit-appearance: none;
  111. appearance: none;
  112. background: transparent;
  113. cursor: pointer;
  114. width: 100%;
  115. }
  116. .radio-player input[type="range"]::-webkit-slider-runnable-track {
  117. background: #2e6d5e;
  118. height: 6px;
  119. border-radius: 3px;
  120. }
  121. .radio-player input[type="range"]::-webkit-slider-thumb {
  122. -webkit-appearance: none;
  123. appearance: none;
  124. background: #fff;
  125. height: 16px;
  126. width: 16px;
  127. border-radius: 50%;
  128. margin-top: -5px;
  129. }
  130. .radio-player input[type="range"]::-moz-range-track {
  131. background: #2e6d5e;
  132. height: 6px;
  133. border-radius: 3px;
  134. }
  135. .radio-player input[type="range"]::-moz-range-thumb {
  136. background: #fff;
  137. height: 16px;
  138. width: 16px;
  139. border-radius: 50%;
  140. border: none;
  141. }
  142. .radio-player input[type="range"]::-moz-range-progress {
  143. background: #fff;
  144. height: 6px;
  145. border-radius: 3px;
  146. }
  147. .radio-player input[type="range"]:disabled::-webkit-slider-runnable-track {
  148. background: #1a3c34;
  149. opacity: 0.7;
  150. }
  151. .radio-player input[type="range"]:disabled::-webkit-slider-thumb {
  152. background: #999;
  153. }
  154. .radio-player input[type="range"]:disabled::-moz-range-track {
  155. background: #1a3c34;
  156. opacity: 0.7;
  157. }
  158. .radio-player input[type="range"]:disabled::-moz-range-thumb {
  159. background: #999;
  160. }
  161. .radio-player input[type="range"]:disabled::-moz-range-progress {
  162. background: #999;
  163. }
  164. .radio-player .volume-icon {
  165. cursor: pointer;
  166. }
  167. .radio-player .volume-icon.muted::before {
  168. content: "🔇";
  169. }
  170. .radio-player .volume-icon:not(.muted)::before {
  171. content: "🔊";
  172. }
  173. .radio-panel input[type="checkbox"] {
  174. margin-right: 5px;
  175. }
  176. .radio-panel-settings {
  177. margin-top: 10px;
  178. display: flex;
  179. gap: 5px;
  180. }
  181. .radio-search {
  182. display: flex;
  183. gap: 5px;
  184. margin: 10px 0;
  185. }
  186. .radio-search input[type="text"] {
  187. flex-grow: 1;
  188. padding: 5px;
  189. border-radius: 5px;
  190. border: 1px solid #ccc;
  191. background-color: #fff;
  192. color: #333;
  193. font-size: 12px;
  194. }
  195. .radio-search button {
  196. background-color: #17a2b8;
  197. color: #fff;
  198. border: none;
  199. border-radius: 5px;
  200. padding: 5px 10px;
  201. cursor: pointer;
  202. font-size: 12px;
  203. }
  204. .radio-search button:hover {
  205. background-color: #138496;
  206. }
  207. .radio-search-results {
  208. max-height: 150px;
  209. overflow-y: auto;
  210. margin-top: 5px;
  211. padding: 5px;
  212. background-color: #2e6d5e;
  213. border-radius: 5px;
  214. }
  215. .radio-search-results div {
  216. display: flex;
  217. justify-content: space-between;
  218. align-items: center;
  219. padding: 5px;
  220. border-bottom: 1px solid #1a3c34;
  221. }
  222. .radio-search-results div:last-child {
  223. border-bottom: none;
  224. }
  225. .radio-search-results button {
  226. background-color: #17a2b8;
  227. color: #fff;
  228. border: none;
  229. border-radius: 5px;
  230. padding: 3px 8px;
  231. cursor: pointer;
  232. font-size: 10px;
  233. }
  234. .radio-search-results button:hover {
  235. background-color: #138496;
  236. }
  237. .notification {
  238. position: fixed;
  239. top: 50px;
  240. right: 10px;
  241. padding: 10px 20px;
  242. border-radius: 5px;
  243. color: white;
  244. z-index: 99999;
  245. }
  246. .notification.success { background-color: #28a745; }
  247. .notification.info { background-color: #17a2b8; }
  248. .notification.warning { background-color: #ffc107; }
  249. .notification.error { background-color: #dc3545; }
  250. `);
  251.  
  252. // === [Эксклюзивное воспроизведение радио во вкладке] ===
  253. const tabId = Date.now().toString();
  254. const MASTER_KEY = '4pda-radio-master';
  255.  
  256. function setAsMaster() {
  257. localStorage.setItem(MASTER_KEY, tabId);
  258. showNotification('Эта вкладка теперь воспроизводит радио', 'success');
  259. }
  260.  
  261. function isMaster() {
  262. return localStorage.getItem(MASTER_KEY) === tabId;
  263. }
  264.  
  265. // === [Инициализация аудиоплеера] ===
  266. let audio = document.getElementById('radioPlayer4PDA');
  267. if (!audio) {
  268. audio = document.createElement('audio');
  269. audio.id = 'radioPlayer4PDA';
  270. document.body.appendChild(audio);
  271. }
  272.  
  273. // Проверка перед запуском радио
  274. const currentMaster = localStorage.getItem(MASTER_KEY);
  275. if (!currentMaster) {
  276. setAsMaster();
  277. } else if (!isMaster()) {
  278. audio.pause();
  279. }
  280.  
  281. // Слушаем изменения мастера
  282. window.addEventListener('storage', (e) => {
  283. if (e.key === MASTER_KEY && e.newValue !== tabId) {
  284. audio.pause();
  285. }
  286. });
  287.  
  288. // Убираем себя из мастеров при закрытии вкладки
  289. window.addEventListener('beforeunload', () => {
  290. if (isMaster()) {
  291. localStorage.removeItem(MASTER_KEY);
  292. }
  293. });
  294.  
  295. // === [Сохраненные настройки] ===
  296. const savedAutoplay = GM_getValue('autoplay', false);
  297. const savedRadio = GM_getValue('radio', '');
  298. const savedVolume = GM_getValue('volume', 1);
  299. const savedTimer = GM_getValue('autotimer', 0);
  300. const savedPlaying = GM_getValue('isPlaying', false);
  301. const savedTime = GM_getValue('currentTime', 0);
  302. let panelPosition = GM_getValue('panelPos', 'top-right');
  303. const panelScale = GM_getValue('panelSize', '1');
  304. const savedCustomStations = GM_getValue('customStations', {});
  305.  
  306. // === [Список радиостанций] ===
  307. let RADIO = {
  308. '🇷🇺 Европа Плюс': 'https://ep256.hostingradio.ru:8052/europaplus256.mp3',
  309. '🇷🇺 Русское Радио': 'https://rusradio.hostingradio.ru/rusradio128.mp3',
  310. '🇷🇺 Юмор FM': 'https://pub0301.101.ru:8443/stream/air/mp3/256/102',
  311. '🇷🇺 Радио Рекорд': 'https://radio-srv1.11one.ru/record192k.mp3',
  312. '🇷🇺 Ретро FM': 'https://retro.hostingradio.ru:8014/retro320.mp3',
  313. '🇷🇺 Радио Шансон': 'https://chanson.hostingradio.ru:8041/chanson256.mp3',
  314. '🇷🇺 DFM Russian Dance': 'https://stream03.pcradio.ru/dfm_russian_dance-hi',
  315. '🇷🇺 DFM': 'https://dfm.hostingradio.ru:80/dfm96.aacp',
  316. '🇷🇺 Дорожное Радио': 'https://dorognoe.hostingradio.ru:8000/dorognoe',
  317. '🇷🇺 Авторадио': 'https://srv01.gpmradio.ru/stream/air/aac/64/100?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiIwZWM3MjU3YTFhNDM5MmMyNWUwZDZkZDQwYjdjNzQ5ZCIsIklQIjoiODEuMTczLjE2NS4yMjUiLCJVQSI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzMuMC4wLjAgU2FmYXJpLzUzNy4zNiIsIlJlZiI6Imh0dHBzOi8vd3d3LmF2dG9yYWRpby5ydS8iLCJ1aWRfY2hhbm5lbCI6IjEwMCIsInR5cGVfY2hhbm5lbCI6ImNoYW5uZWwiLCJ0eXBlRGV2aWNlIjoiUEMiLCJCcm93c2VyIjoiQ2hyb21lIiwiQnJvd3NlclZlcnNpb24iOiIxMzMuMC4wLjAiLCJTeXN0ZW0iOiJNYWMgT1MgWCBQdW1hIiwiZXhwIjoxNzQyNjcxOTc1fQ.b1Hha0aGp4hWbgFELSzEapRcpOoejzs8tmdDARY0JyA',
  318. '🇩🇪 Радио Картина': 'https://rs.kartina.tv/kartina_320kb',
  319. '🇰🇿 LuxFM': 'https://icecast.luxfm.kz/luxfm',
  320. '🇰🇿 Radio NS': 'https://icecast.ns.kz/radions',
  321. '🇰🇿 NRJ Kazakhstan': 'https://stream03.pcradio.ru/energyfm_ru-med',
  322. '🇰🇿 Радио Жаңа FM': 'https://live.zhanafm.kz:8443/zhanafm_onair',
  323. '🇺🇦 Хіт FM': 'http://online.hitfm.ua/HitFM',
  324. '🇺🇦 Kiss FM UA': 'http://online.kissfm.ua/KissFM'
  325. };
  326.  
  327. // Объединяем предопределенные станции с пользовательскими
  328. Object.assign(RADIO, savedCustomStations);
  329.  
  330. // === [Проверка доступности радиопотоков] ===
  331. async function checkStream(url) {
  332. return true; // Заглушка, можно добавить реальную проверку
  333. }
  334.  
  335. async function validateStations() {
  336. const validStations = {};
  337. for (const [name, url] of Object.entries(RADIO)) {
  338. if (await checkStream(url)) {
  339. validStations[name] = url;
  340. } else {
  341. showNotification(`Радиостанция ${name} недоступна`, 'warning');
  342. }
  343. }
  344. RADIO = validStations;
  345. updateStationList();
  346. }
  347.  
  348. // === [Динамическое обновление списка радиостанций] ===
  349. async function loadStations() {
  350. try {
  351. const response = await new Promise((resolve) => {
  352. setTimeout(() => resolve({ ok: true, json: () => Promise.resolve(RADIO) }), 1000);
  353. });
  354. if (response.ok) {
  355. RADIO = await response.json();
  356. await validateStations();
  357. showNotification('Список радиостанций обновлен', 'success');
  358. } else {
  359. showNotification('Ошибка загрузки списка радиостанций', 'error');
  360. }
  361. } catch (error) {
  362. console.error('Ошибка обновления радиостанций:', error);
  363. showNotification('Ошибка обновления радиостанций', 'error');
  364. }
  365. }
  366.  
  367. // === [Уведомления] ===
  368. function showNotification(message, type) {
  369. const notification = document.createElement('div');
  370. notification.className = `notification ${type}`;
  371. notification.textContent = message;
  372. document.body.appendChild(notification);
  373. setTimeout(() => notification.remove(), 3000);
  374. }
  375.  
  376. // === [Преобразование кода страны в флаг] ===
  377. function countryCodeToFlag(countryCode) {
  378. if (!countryCode || countryCode.length !== 2) {
  379. return '🌐'; // Нейтральный флаг, если код страны отсутствует
  380. }
  381.  
  382. const codePoints = countryCode
  383. .toUpperCase()
  384. .split('')
  385. .map(char => 0x1F1E6 + (char.charCodeAt(0) - 65)); // 'A' → 0x1F1E6, 'B' → 0x1F1E7, ..., 'U' → 0x1F1FA
  386. return String.fromCodePoint(...codePoints);
  387. }
  388.  
  389. // === [Поиск через RadioBrowser API] ===
  390. function searchStations(query, callback) {
  391. showNotification('Поиск...', 'info');
  392. GM_xmlhttpRequest({
  393. method: 'GET',
  394. url: `https://de1.api.radio-browser.info/json/stations/search?name=${encodeURIComponent(query)}&limit=10`,
  395. onload: function(response) {
  396. try {
  397. const data = JSON.parse(response.responseText);
  398. const results = data.map(station => ({
  399. name: station.name,
  400. url: station.url_resolved,
  401. countryCode: station.countrycode || ''
  402. }));
  403. callback(results);
  404. } catch (error) {
  405. console.error('Ошибка парсинга ответа API:', error);
  406. showNotification('Ошибка поиска радиостанций', 'error');
  407. callback([]);
  408. }
  409. },
  410. onerror: function(error) {
  411. console.error('Ошибка запроса к API:', error);
  412. showNotification('Ошибка поиска радиостанций', 'error');
  413. callback([]);
  414. }
  415. });
  416. }
  417.  
  418. function addStation(name, url, countryCode) {
  419. // Добавляем флаг к имени станции
  420. const flag = countryCodeToFlag(countryCode);
  421. const stationNameWithFlag = `${flag} ${name}`;
  422.  
  423. if (RADIO[stationNameWithFlag]) {
  424. showNotification('Радиостанция уже добавлена', 'warning');
  425. return;
  426. }
  427.  
  428. RADIO[stationNameWithFlag] = url;
  429. const customStations = GM_getValue('customStations', {});
  430. customStations[stationNameWithFlag] = url;
  431. GM_setValue('customStations', customStations);
  432. updateStationList();
  433. showNotification(`Радиостанция ${stationNameWithFlag} добавлена`, 'success');
  434. }
  435.  
  436. // === [Интерфейс] ===
  437. function createInterface() {
  438. // Панель радио
  439. const panel = document.createElement('div');
  440. panel.className = 'radio-panel';
  441. panel.style.display = GM_getValue('panelVisible', false) ? 'block' : 'none';
  442.  
  443. // Кнопка S
  444. const toggleButton = document.createElement('button');
  445. toggleButton.className = 'radio-toggle-button';
  446. toggleButton.textContent = '🎧';
  447. toggleButton.onclick = () => {
  448. if (!panel) {
  449. console.error('Панель не найдена');
  450. showNotification('Ошибка: панель не создана', 'error');
  451. return;
  452. }
  453. panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  454. GM_setValue('panelVisible', panel.style.display === 'block');
  455. showNotification(`Панель ${panel.style.display === 'block' ? 'открыта' : 'закрыта'}`, 'info');
  456. };
  457. document.body.appendChild(toggleButton);
  458.  
  459. // Применение позиции и масштаба
  460. updatePanelPosition(panel, panelPosition);
  461. updatePanelScale(panel, panelScale);
  462.  
  463. // Заголовок
  464. const header = document.createElement('div');
  465. header.className = 'radio-panel-header';
  466. header.innerHTML = '<span>⚡ Громкость:</span>';
  467. panel.appendChild(header);
  468.  
  469. // Кнопки пресетов громкости
  470. const controls = document.createElement('div');
  471. controls.className = 'radio-panel-controls';
  472. ['Тихо', 'Комфорт', 'Громко'].forEach((label, index) => {
  473. const button = document.createElement('button');
  474. button.textContent = label;
  475. button.onclick = () => {
  476. if (!audio) {
  477. console.error('Аудиоплеер не инициализирован');
  478. showNotification('Ошибка: аудиоплеер не доступен', 'error');
  479. return;
  480. }
  481. const volumes = [0.2, 0.5, 0.8];
  482. audio.volume = volumes[index];
  483. GM_setValue('volume', audio.volume);
  484. updateVolumeSlider(audio.volume);
  485. showNotification(`Громкость: ${label} (${volumes[index] * 100}%)`, 'info');
  486. };
  487. controls.appendChild(button);
  488. });
  489. panel.appendChild(controls);
  490.  
  491. // Ползунок громкости
  492. const volumeSlider = document.createElement('input');
  493. volumeSlider.type = 'range';
  494. volumeSlider.min = '0';
  495. volumeSlider.max = '1';
  496. volumeSlider.step = '0.01';
  497. volumeSlider.value = savedVolume;
  498. volumeSlider.oninput = () => {
  499. if (!audio) {
  500. console.error('Аудиоплеер не инициализирован');
  501. showNotification('Ошибка: аудиоплеер не доступен', 'error');
  502. return;
  503. }
  504. audio.volume = volumeSlider.value;
  505. GM_setValue('volume', audio.volume);
  506. };
  507. controls.appendChild(volumeSlider);
  508.  
  509. // Выбор радиостанции
  510. const stationSelect = document.createElement('select');
  511. stationSelect.id = 'radioStationSelect';
  512. updateStationList();
  513. controls.appendChild(stationSelect);
  514. panel.appendChild(controls);
  515.  
  516. // Поиск радиостанций
  517. const searchSection = document.createElement('div');
  518. searchSection.className = 'radio-search';
  519. const searchInput = document.createElement('input');
  520. searchInput.type = 'text';
  521. searchInput.placeholder = 'Поиск радиостанций...';
  522. searchSection.appendChild(searchInput);
  523. const searchButton = document.createElement('button');
  524. searchButton.textContent = 'Поиск';
  525. searchSection.appendChild(searchButton);
  526. panel.appendChild(searchSection);
  527.  
  528. // Результаты поиска
  529. const searchResults = document.createElement('div');
  530. searchResults.className = 'radio-search-results';
  531. searchResults.style.display = 'none';
  532. panel.appendChild(searchResults);
  533.  
  534. searchButton.onclick = () => {
  535. const query = searchInput.value.trim();
  536. if (!query) {
  537. showNotification('Введите запрос для поиска', 'warning');
  538. return;
  539. }
  540.  
  541. searchStations(query, (results) => {
  542. searchResults.innerHTML = '';
  543. searchResults.style.display = results.length ? 'block' : 'none';
  544.  
  545. if (!results.length) {
  546. showNotification('Радиостанции не найдены', 'info');
  547. return;
  548. }
  549.  
  550. results.forEach(station => {
  551. const resultItem = document.createElement('div');
  552. const flag = countryCodeToFlag(station.countryCode);
  553. resultItem.textContent = `${flag} ${station.name}`;
  554. const addButton = document.createElement('button');
  555. addButton.textContent = 'Добавить';
  556. addButton.onclick = () => {
  557. addStation(station.name, station.url, station.countryCode);
  558. searchResults.style.display = 'none';
  559. searchInput.value = '';
  560. };
  561. resultItem.appendChild(addButton);
  562. searchResults.appendChild(resultItem);
  563. });
  564. });
  565. };
  566.  
  567. // Плеер
  568. const player = document.createElement('div');
  569. player.className = 'radio-player';
  570. const playButton = document.createElement('button');
  571. playButton.textContent = savedPlaying ? '⏸' : '▶';
  572. playButton.onclick = togglePlay;
  573. player.appendChild(playButton);
  574. const timeSlider = document.createElement('input');
  575. timeSlider.type = 'range';
  576. timeSlider.min = '0';
  577. timeSlider.max = '100';
  578. timeSlider.value = '0';
  579. timeSlider.disabled = true;
  580. player.appendChild(timeSlider);
  581. const timeDisplay = document.createElement('span');
  582. timeDisplay.textContent = '0:00';
  583. player.appendChild(timeDisplay);
  584. const volumeIcon = document.createElement('span');
  585. volumeIcon.className = 'volume-icon';
  586. volumeIcon.onclick = () => {
  587. if (!audio) {
  588. console.error('Аудиоплеер не инициализирован');
  589. showNotification('Ошибка: аудиоплеер не доступен', 'error');
  590. return;
  591. }
  592. audio.muted = !audio.muted;
  593. volumeIcon.classList.toggle('muted', audio.muted);
  594. showNotification(audio.muted ? 'Звук выключен' : 'Звук включен', 'info');
  595. };
  596. player.appendChild(volumeIcon);
  597. panel.appendChild(player);
  598.  
  599. // Таймер, автостарт и обновление
  600. const footer = document.createElement('div');
  601. footer.className = 'radio-panel-controls';
  602. const timerSelect = document.createElement('select');
  603. timerSelect.innerHTML = `
  604. <option value="0">Без таймера</option>
  605. <option value="10">10 мин</option>
  606. <option value="30">30 мин</option>
  607. <option value="60">60 мин</option>
  608. `;
  609. timerSelect.value = savedTimer;
  610. timerSelect.onchange = () => {
  611. GM_setValue('autotimer', parseInt(timerSelect.value) || 0);
  612. setAutoTimer(parseInt(timerSelect.value) || 0);
  613. };
  614. footer.appendChild(timerSelect);
  615. const autostartLabel = document.createElement('label');
  616. const autostartCheckbox = document.createElement('input');
  617. autostartCheckbox.type = 'checkbox';
  618. autostartCheckbox.checked = savedAutoplay;
  619. autostartCheckbox.onchange = () => {
  620. GM_setValue('autoplay', autostartCheckbox.checked);
  621. };
  622. autostartLabel.appendChild(autostartCheckbox);
  623. autostartLabel.appendChild(document.createTextNode('Автостарт'));
  624. footer.appendChild(autostartLabel);
  625. const refreshButton = document.createElement('button');
  626. refreshButton.textContent = '↻';
  627. refreshButton.title = 'Обновить станции';
  628. refreshButton.onclick = loadStations;
  629. footer.appendChild(refreshButton);
  630. panel.appendChild(footer);
  631.  
  632. // Настройки панели
  633. const settings = document.createElement('div');
  634. settings.className = 'radio-panel-settings';
  635. const positionSelect = document.createElement('select');
  636. positionSelect.innerHTML = `
  637. <option value="top-left">Вверху слева</option>
  638. <option value="top-center">Вверху посередине</option>
  639. <option value="top-right">Вверху справа</option>
  640. `;
  641. positionSelect.value = panelPosition;
  642. positionSelect.onchange = () => {
  643. GM_setValue('panelPos', positionSelect.value);
  644. panelPosition = positionSelect.value;
  645. updatePanelPosition(panel, positionSelect.value);
  646. showNotification(`Панель перемещена: ${positionSelect.options[positionSelect.selectedIndex].text}`, 'info');
  647. };
  648. settings.appendChild(positionSelect);
  649. const scaleSelect = document.createElement('select');
  650. scaleSelect.innerHTML = `
  651. <option value="0.8">Маленький</option>
  652. <option value="1">Средний</option>
  653. <option value="1.1">Большой</option>
  654. `;
  655. scaleSelect.value = panelScale;
  656. scaleSelect.onchange = () => {
  657. GM_setValue('panelSize', scaleSelect.value);
  658. updatePanelScale(panel, scaleSelect.value);
  659. showNotification(`Масштаб панели: ${scaleSelect.options[scaleSelect.selectedIndex].text}`, 'info');
  660. };
  661. settings.appendChild(scaleSelect);
  662. panel.appendChild(settings);
  663.  
  664. document.body.appendChild(panel);
  665.  
  666. // Функция для обновления ползунка громкости
  667. function updateVolumeSlider(value) {
  668. volumeSlider.value = value;
  669. }
  670. }
  671.  
  672. function updatePanelPosition(panel, position) {
  673. if (!panel) {
  674. console.error('Панель не найдена для обновления позиции');
  675. return;
  676. }
  677. panel.style.top = '10px';
  678. panel.style.bottom = '';
  679. panel.style.left = '';
  680. panel.style.right = '';
  681. panel.style.transform = '';
  682. switch (position) {
  683. case 'top-left':
  684. panel.style.left = '10px';
  685. break;
  686. case 'top-center':
  687. panel.style.left = '50%';
  688. panel.style.transform = 'translateX(-50%)';
  689. break;
  690. case 'top-right':
  691. panel.style.right = '10px';
  692. break;
  693. }
  694. }
  695.  
  696. function updatePanelScale(panel, scale) {
  697. if (!panel) {
  698. console.error('Панель не найдена для обновления масштаба');
  699. return;
  700. }
  701. panel.style.transform = `scale(${scale})`;
  702. panel.style.transformOrigin = panelPosition.includes('left') ? 'top left' : panelPosition.includes('right') ? 'top right' : 'top center';
  703. if (parseFloat(scale) > 1) {
  704. panel.style.maxWidth = '80vw';
  705. if (panelPosition === 'top-center') {
  706. panel.style.left = '50%';
  707. panel.style.transform = `translateX(-50%) scale(${scale})`;
  708. }
  709. } else {
  710. panel.style.maxWidth = '90vw';
  711. }
  712. }
  713.  
  714. function updateStationList() {
  715. const stationSelect = document.getElementById('radioStationSelect');
  716. if (!stationSelect) {
  717. return;
  718. }
  719. stationSelect.innerHTML = '<option value="">Выберите радиостанцию</option>';
  720. Object.keys(RADIO).forEach(name => {
  721. const option = document.createElement('option');
  722. option.value = RADIO[name];
  723. option.textContent = name;
  724. if (RADIO[name] === savedRadio) option.selected = true;
  725. stationSelect.appendChild(option);
  726. });
  727. stationSelect.onchange = () => {
  728. if (stationSelect.value) {
  729. audio.src = stationSelect.value;
  730. GM_setValue('radio', stationSelect.value);
  731. if (savedAutoplay || savedPlaying) {
  732. audio.play().catch(e => {
  733. console.error('Ошибка воспроизведения:', e);
  734. showNotification('Ошибка воспроизведения радиостанции', 'error');
  735. });
  736. }
  737. }
  738. };
  739. }
  740.  
  741. // === [Управление воспроизведением] ===
  742. function togglePlay() {
  743. const playButton = document.querySelector('.radio-player button');
  744. if (!playButton) {
  745. return;
  746. }
  747. if (audio.paused) {
  748. if (isMaster()) {
  749. audio.play().catch(e => {
  750. console.error('Ошибка воспроизведения:', e);
  751. showNotification('Ошибка воспроизведения радиостанции', 'error');
  752. });
  753. GM_setValue('isPlaying', true);
  754. playButton.textContent = '⏸';
  755. }
  756. } else {
  757. audio.pause();
  758. GM_setValue('isPlaying', false);
  759. playButton.textContent = '▶';
  760. }
  761. }
  762.  
  763. // === [Таймер автовыключения] ===
  764. let timerId;
  765. function setAutoTimer(minutes) {
  766. clearTimeout(timerId);
  767. if (minutes > 0) {
  768. timerId = setTimeout(() => {
  769. audio.pause();
  770. GM_setValue('isPlaying', false);
  771. const playButton = document.querySelector('.radio-player button');
  772. if (playButton) {
  773. playButton.textContent = '▶';
  774. }
  775. showNotification('Радио остановлено по таймеру', 'info');
  776. }, minutes * 60 * 1000);
  777. }
  778. }
  779.  
  780. // === [Инициализация] ===
  781. try {
  782. createInterface();
  783.  
  784. // Инициализация аудио
  785. audio.volume = savedVolume;
  786. if (savedRadio) {
  787. audio.src = savedRadio;
  788. if (savedAutoplay && isMaster()) {
  789. audio.play().catch(e => {
  790. console.error('Ошибка воспроизведения:', e);
  791. showNotification('Ошибка воспроизведения радиостанции', 'error');
  792. });
  793. }
  794. }
  795. audio.ontimeupdate = () => GM_setValue('currentTime', audio.currentTime);
  796. audio.onerror = () => {
  797. console.error('Ошибка загрузки радиопотока');
  798. showNotification('Ошибка загрузки радиопотока', 'error');
  799. };
  800.  
  801. // Обновление списка радиостанций без проверки
  802. updateStationList();
  803. setAutoTimer(savedTimer);
  804. } catch (error) {
  805. console.error('Критическая ошибка инициализации:', error);
  806. showNotification('Ошибка запуска скрипта', 'error');
  807. }
  808. })();