TrixMusic

TriX Executor's Music Player.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         TrixMusic
// @namespace    http://tampermonkey.net/
// @version      1.1.7
// @description  TriX Executor's Music Player.
// @author       Painsel
// @match        *://*/*
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://code.jquery.com/ui/1.13.2/jquery-ui.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.min.js
// @resource     JQUERY_UI_CSS https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_info
// ==/UserScript==

/*
  Copyright (c) 2025 Painsel
  All Rights Reserved.

  This script is proprietary software. You may not use, copy, modify, merge,
  publish, distribute, sublicense, and/or sell copies of the Software.

  UNAUTHORIZED COPYING, DISTRIBUTION, OR MODIFICATION OF THIS SCRIPT,
  EITHER IN WHOLE OR IN PART, IS STRICTLY PROHIBITED.
*/


(function() {
    'use strict';

    // --- SETTINGS GATEKEEPER ---
    const showOnAllSites = GM_getValue('trixShowOnAllSites', true);

    if (!showOnAllSites) {
        const currentUrl = window.location.href;
        const allowedSites = [
            'https://territorial.io/',
            'https://fxclient.github.io/FXclient/'
        ];
        const isAllowed = allowedSites.some(site => currentUrl.startsWith(site));

        if (!isAllowed) {
            console.log("TrixMusic: Execution stopped. This site is not in the allowed list.");
            return;
        }
    }

    // --- 1. INJECT STYLESHEETS & FONTS ---

    const jqueryUiCss = GM_getResourceText("JQUERY_UI_CSS");
    GM_addStyle(jqueryUiCss);

    const faLink = document.createElement('link');
    faLink.setAttribute('rel', 'stylesheet');
    faLink.setAttribute('href', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css');
    document.head.appendChild(faLink);

    const fontLink = document.createElement('link');
    fontLink.setAttribute('rel', 'stylesheet');
    fontLink.setAttribute('href', 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700');
    document.head.appendChild(fontLink);

    // --- 2. DEFINE HTML AND CSS ---

    const playerHTML = `
        <div class="cont trix-music-container" style="display: none;">
          <div class="player-main">
              <div class="cover">
                <div class="bg"></div>
                <div class="middle">
                  <div class="image">
                    <img src="" id="cover"/>
                  </div>
                  <h1>...</h1>
                  <h2>...</h2>
                </div>
              </div>
              <div class="bar">
                <div class="m_slider"></div>
                <div class="time-display"><span class="current-time">0:00</span> / <span class="total-time">0:00</span></div>
                <div class="buttons">
                  <a class="btn random"><i class="fa fa-random"></i></a>
                  <a class="btn prev"><i class="fa fa-step-backward"></i></a>
                  <a class="btn play"><i class="fa fa-play"></i><i class="fa fa-pause"></i></a>
                  <a class="btn next"><i class="fa fa-step-forward"></i></a>
                  <a class="btn loop active" title="Loop Playlist (Autoplay Next)"><i class="fa fa-refresh"></i></a>
                  <a class="btn volume"><i class="fa fa-volume-up"></i><i class="fa fa-volume-off"></i></a>
                </div>
              </div>
          </div>
          <div class="list-area">
              <ul class="list"></ul>
              <div class="suggestion-box">
                  <p>Have any suggestions? Send a message to Painsel!</p>
                  <a href="https://form.jotform.com/253124626389563" class="suggestion-btn" target="_blank" rel="noopener noreferrer">Add a Song</a>
                  <div class="settings-area">
                      <label>
                          <input type="checkbox" id="trix-show-on-all-sites">
                          Show on All Websites
                      </label>
                  </div>
                  <div id="trixmusic-version" class="version-footer"></div>
                  <div id="trixmusic-update-footer" class="update-footer" style="display: none;">
                      <span>New Version available. Update now?</span>
                      <a href="https://update.greasyfork.org/scripts/555311/TrixMusic.user.js" class="update-footer-btn" target="_blank">Update</a>
                  </div>
              </div>
          </div>
        </div>
        <button id="trixmusic-toggle-button">Toggle TrixMusic</button>
    `;

    const playerCSS = `
        #trixmusic-toggle-button {
            position: fixed; bottom: 15px; right: 15px; z-index: 99998;
            padding: 10px 15px; background-color: #FE186B; color: white;
            border: none; border-radius: 5px; cursor: move; font-family: 'Roboto', sans-serif;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
        }
        .trix-music-container * { position: relative; cursor: default; box-sizing: border-box; }
        .trix-music-container {
            position: fixed; background: #fff; width: 950px; height: 500px;
            box-shadow: 0px 0px 50px #aaa; font-family: 'Roboto', sans-serif;
            z-index: 99999; cursor: grab; display: flex; overflow: hidden;
        }
        .trix-music-container:active { cursor: grabbing; }

        /* Main Player Area (Left Side) */
        .player-main { width: 550px; height: 100%; display: flex; flex-direction: column; }
        .player-main .cover { flex-grow: 1; background: #000; overflow: hidden; transition: 0.5s; z-index: 10; }
        .player-main .cover .bg {
            width: 100%; height: 100%; opacity: 0.5;
            filter: blur(10px); transform: scale(1.2); z-index: 10;
            background-size: cover; background-position: center;
        }
        .player-main .cover .middle {
            position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
            text-align: center; color: #fff; transition: 0.5s; z-index: 30; width: 100%;
        }
        .player-main .cover .middle .image { width: 200px; height: 200px; overflow: hidden; border-radius: 50%; margin: 0 auto; }
        .player-main .cover .middle .image img { display: block; width: 100%; }
        .player-main .cover .middle h1 { font-size: 20px; padding: 10px 0; }
        .player-main .cover .middle h2 { font-size: 12px; opacity: 0.5; }

        /* Bar */
        .player-main .bar {
            width: 100%; height: 100px; position: relative; bottom: 0; left: 0;
            text-align: center; transition: 0.5s; z-index: 20; color: #fff; background: #000;
        }
        .player-main .bar .m_slider { background: #555; width: 100%; height: 5px; cursor: pointer;}
        .player-main .bar .m_slider .ui-slider-range { height: 100%; background: #FE186B; }
        .player-main .bar .m_slider .ui-slider-handle {
            width: 15px; height: 15px; top: -5px; margin-left: -8px;
            border-radius: 50%; display: block; background: #FFF; border: none;
        }
        .player-main .bar .buttons { width: 100%; top: 20px; }
        .player-main .bar .btn {
            display: inline-block; width: 30px; height: 30px; text-align: center;
            overflow: hidden; vertical-align: middle; margin: 0px 5px;
            transition: 0.5s; cursor: pointer; color: #fff;
        }
        .player-main .bar .btn:hover { color: #FE186B; }
        .player-main .bar .btn.toggle i { top: -30px; }
        .player-main .bar .btn i { display: block; line-height: 30px; top: 0px; transition: 0.5s; pointer-events: none; }
        .player-main .bar .btn.play { width: 50px; height: 50px; font-size: 30px; margin: 0 8px; }
        .player-main .bar .btn.play i { line-height: 50px; }
        .player-main .bar .btn.play.toggle i { top: -50px; }
        .player-main .bar .btn.random { position: absolute; left: 20px; top: 10px; }
        .player-main .bar .btn.volume { position: absolute; right: 20px; top: 10px; }
        .player-main .bar .btn.loop { position: absolute; right: 60px; top: 10px; opacity: 0.3; }
        .player-main .bar .btn.loop.active { opacity: 1; }
        .player-main .bar .btn.loop i.fa-repeat { font-size: 1.1em; }
        .player-main .bar .time-display {
            position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
            font-size: 12px; color: white; opacity: 0.7;
        }

        /* List Area & Suggestion Box (Right Side) */
        .list-area {
            position: relative; width: 400px; height: 100%; z-index: 20;
            padding: 10px; box-sizing: border-box; display: flex; flex-direction: column; background: #f7f7f7;
        }
        .list { list-style: none; flex-grow: 1; overflow-y: auto; margin: 0; padding: 0; }
        .list li { background: rgba(255, 255, 255, 0.9); width: 100%; margin-top: 10px; transition: background 0.5s; cursor: pointer; }
        .list li * { pointer-events: none; }
        .list li .text { height: 50px; display: inline-block; width: calc(100% - 55px); padding: 5px 10px; overflow: hidden; }
        .list li .text p { font-size: 16px; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #333; }
        .list li .text p + p { font-size: 12px; opacity: 0.6; }
        .list li.playing { color: #FE186B; background: #fff; box-shadow: 0 0 5px rgba(254, 24, 107, 0.5); }
        .list li.playing .pl { opacity: 1; }
        .list li:hover { background: #eee; }
        .list li .pl { transition: 0.5s; position: absolute; right: 10px; top: 0; line-height: 50px; opacity: 0; }
        .list li img { height: 50px; vertical-align: top; display: inline-block; }
        .suggestion-box {
            padding: 15px 10px 5px 10px; text-align: center; flex-shrink: 0;
            border-top: 1px solid #eee; margin-top: 10px;
        }
        .suggestion-box p { font-size: 12px; color: #666; margin: 0 0 10px 0; }
        .suggestion-btn {
            display: inline-block; padding: 8px 15px; background-color: #FE186B;
            color: #fff !important; text-decoration: none; border-radius: 5px;
            font-size: 13px; font-weight: 500; transition: background-color 0.2s ease; cursor: pointer;
        }
        .suggestion-btn:hover { background-color: #d11458; }
        .settings-area { margin-top: 15px; font-size: 12px; color: #555; }
        .settings-area input { vertical-align: middle; margin-right: 5px; }
        .version-footer { font-size: 11px; color: #aaa; margin-top: 8px; }
        .update-footer {
            font-size: 11px; color: #c0392b; margin-top: 8px; padding: 5px;
            background-color: #f9e3e3; border-radius: 4px; display: flex;
            justify-content: space-between; align-items: center;
        }
        .update-footer-btn {
            text-decoration: none; background-color: #27ae60; color: #fff !important;
            padding: 3px 8px; border-radius: 4px; font-size: 11px;
            margin-left: 10px; cursor: pointer;
        }
        .update-footer-btn:hover { background-color: #2ecc71; }
    `;

    // --- UPDATE CHECKER LOGIC ---
    const updateModalCSS = `
        .trix-update-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.7); z-index: 100000; display: flex;
            align-items: center; justify-content: center;
        }
        .trix-update-modal {
            background: #fff; padding: 25px; border-radius: 8px; text-align: center;
            max-width: 400px; font-family: 'Roboto', sans-serif;
        }
        .trix-update-modal h2 { margin-top: 0; color: #c0392b; }
        .trix-update-modal p { margin-bottom: 20px; color: #333; }
        .trix-update-modal .modal-buttons button {
            padding: 10px 20px; border: none; border-radius: 5px;
            cursor: pointer; font-size: 14px; margin: 0 10px;
        }
        .trix-update-modal .update-btn { background-color: #27ae60; color: white; }
        .trix-update-modal .later-btn { background-color: #7f8c8d; color: white; }
    `;

    const updateModalHTML = (updateURL) => `
        <div class="trix-update-overlay">
            <div class="trix-update-modal">
                <h2>OUTDATED VERSION</h2>
                <p>You are using an outdated version of TrixMusic. Please update for the best experience!</p>
                <div class="modal-buttons">
                    <button class="update-btn" onclick="window.open('${updateURL}', '_blank');">Update now!</button>
                    <button class="later-btn">Remind me later!</button>
                </div>
            </div>
        </div>
    `;

    function showUpdateNotification(updateURL) {
        GM_addStyle(updateModalCSS);
        const modalElement = document.createElement('div');
        modalElement.innerHTML = updateModalHTML(updateURL);
        document.body.appendChild(modalElement);

        modalElement.querySelector('.later-btn').addEventListener('click', () => modalElement.remove());
        modalElement.querySelector('.update-btn').addEventListener('click', () => modalElement.remove());
        $('#trixmusic-update-footer').show();
    }

    function checkForUpdates() {
        const currentVersion = GM_info.script.version;
        const updateURL = 'https://update.greasyfork.org/scripts/555311/TrixMusic.user.js';

        GM_xmlhttpRequest({
            method: 'GET',
            url: updateURL,
            onload: function(response) {
                const match = response.responseText.match(/@version\s+([\d.]+)/);
                if (match) {
                    const latestVersion = match[1];
                    if (latestVersion > currentVersion) {
                        showUpdateNotification(updateURL);
                    }
                }
            }
        });
    }

    // --- 3. INJECT HTML & SCRIPT LOGIC ---

    document.body.insertAdjacentHTML('beforeend', playerHTML);
    GM_addStyle(playerCSS);

    $(function() {
        // --- Player State and Data ---
        const player = {
            tracklist: [
                {
                    title: 'Passo Bem Solto',
                    artist: 'ATLXS',
                    cover: 'https://upload.wikimedia.org/wikipedia/en/thumb/3/34/Passo_Bem_Solto_-_Atlxs.jpg/250px-Passo_Bem_Solto_-_Atlxs.jpg',
                    file: 'https://evzxirgylircpnblikrw.supabase.co/storage/v1/object/public/MOOZIK/ATLXS_-_PASSO_BEM_SOLTO_-_Slowed_@BaseNaija%20(2).mp3'
                },
                {
                    title: 'Honeypie',
                    artist: 'JAWNY',
                    cover: 'https://i1.sndcdn.com/artworks-8JDT4NmXVBHXwi02-5YRUCA-t500x500.jpg',
                    file: 'https://evzxirgylircpnblikrw.supabase.co/storage/v1/object/public/MOOZIK/JAWNY_-_Honeypie_muzonov.net_(mp3.pm).mp3'
                },
                {
                    title: 'It Has To Be This Way',
                    artist: 'Jamie Christopherson & Logan Mader',
                    cover: 'https://i1.sndcdn.com/artworks-000072742433-j335oj-t500x500.jpg',
                    file: 'https://evzxirgylircpnblikrw.supabase.co/storage/v1/object/public/MOOZIK/Metal%20Gear%20Rising-%20Revengeance%20OST%20-%20It%20Has%20To%20Be%20This%20Way%20_Senator%20Battle_%20-%20Jamie%20Christopherson%20-%20SoundLoadMate.com.mp3'
                },
                {
                    title: 'D.D.D.D',
                    artist: '(K)NoW_NAME',
                    cover: 'https://i.scdn.co/image/ab67616d0000b2731b0339f042d798da979c898f',
                    file: 'https://evzxirgylircpnblikrw.supabase.co/storage/v1/object/public/MOOZIK/D.D.D.D..mp3'
                }
            ],
            currentIndex: 0,
            currentSound: null,
            isPlaying: false,
            loopMode: 1, // 0 = off, 1 = loop playlist (autoplay next), 2 = loop single
            isRandom: false,
            isMuted: false
        };

        // --- Core Functions ---
        const loadTrack = (index, shouldPlay) => {
            if (player.currentSound) player.currentSound.unload();
            const track = player.tracklist[index];
            player.currentIndex = index;

            $('.cover .image img').attr('src', track.cover);
            $('.cover .bg').css('background-image', 'url(' + track.cover + ')');
            $('.cover h1').text(track.title);
            $('.cover h2').text(track.artist);
            $('.list li').removeClass('playing');
            $(`.list li#track-${index}`).addClass('playing');

            player.currentSound = new Howl({
                src: [track.file],
                html5: true,
                volume: player.isMuted ? 0 : 1,
                loop: player.loopMode === 2,
                onplay: () => {
                    player.isPlaying = true;
                    $('.play').addClass('toggle');
                    requestAnimationFrame(step);
                },
                onpause: () => {
                    player.isPlaying = false;
                    $('.play').removeClass('toggle');
                },
                onend: () => {
                    if (player.loopMode === 1) {
                        nextTrack();
                    } else if (player.loopMode === 0) {
                        if (!player.isRandom && player.currentIndex < player.tracklist.length - 1) {
                            nextTrack();
                        } else if (player.isRandom) {
                            nextTrack();
                        } else {
                            player.isPlaying = false;
                            $('.play').removeClass('toggle');
                        }
                    }
                },
                onload: () => {
                    updateTimeDisplay();
                    if (shouldPlay) playTrack();
                }
            });
        };

        const playTrack = () => { if (player.currentSound && !player.currentSound.playing()) player.currentSound.play(); };
        const pauseTrack = () => { if (player.currentSound && player.currentSound.playing()) player.currentSound.pause(); };

        const nextTrack = () => {
            let nextIndex;
            if (player.isRandom) {
                do { nextIndex = Math.floor(Math.random() * player.tracklist.length); } while (nextIndex === player.currentIndex && player.tracklist.length > 1);
            } else { nextIndex = (player.currentIndex + 1) % player.tracklist.length; }
            loadTrack(nextIndex, true);
        };

        const prevTrack = () => {
            const prevIndex = (player.currentIndex - 1 + player.tracklist.length) % player.tracklist.length;
            loadTrack(prevIndex, true);
        };

        const seek = (value) => {
            if (player.currentSound && player.currentSound.state() === 'loaded') {
                player.currentSound.seek(player.currentSound.duration() * (value / 100));
            }
        };

        // --- UI Update Functions ---
        const formatTime = (secs) => {
            const minutes = Math.floor(secs / 60) || 0;
            const seconds = Math.floor(secs - minutes * 60) || 0;
            return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
        };

        const updateTimeDisplay = () => {
            if (!player.currentSound) return;
            const duration = player.currentSound.duration() || 0;
            const seekPos = player.currentSound.seek() || 0;
            $('.current-time').text(formatTime(seekPos));
            $('.total-time').text(formatTime(duration));
        };

        const step = () => {
            if (player.currentSound && player.currentSound.playing()) {
                const seekPos = player.currentSound.seek() || 0;
                const duration = player.currentSound.duration() || 0;
                if (duration) $('.m_slider').slider('value', (seekPos / duration) * 100);
                updateTimeDisplay();
                requestAnimationFrame(step);
            }
        };

        // --- Initialization ---
        const container = $('.trix-music-container');
        container.css({
            top: ($(window).height() - container.outerHeight()) / 2 + 'px',
            left: ($(window).width() - container.outerWidth()) / 2 + 'px'
        });

        $('#trixmusic-version').text('v' + GM_info.script.version);

        $.each(player.tracklist, (i, track) => {
            $('.list').append(
                `<li id="track-${i}">
                    <img src="${track.cover}"/>
                    <div class="text"><p>${track.title}</p><p>${track.artist}</p></div>
                    <i class="fa fa-music pl"></i>
                </li>`
            );
        });

        $(".m_slider").slider({
            range: "min", value: 0, min: 0, max: 100,
            slide: (event, ui) => {
                const newTime = player.currentSound.duration() * (ui.value / 100);
                $('.current-time').text(formatTime(newTime));
            },
            stop: (event, ui) => {
                seek(ui.value);
            }
        });
        loadTrack(0, false);

        // --- Event Handlers ---
        $('.play').on('click', () => (player.currentSound.playing()) ? pauseTrack() : playTrack());
        $('.next').on('click', nextTrack);
        $('.prev').on('click', prevTrack);

        $('.volume').on('click', function() {
            $(this).toggleClass('toggle');
            player.isMuted = $(this).hasClass('toggle');
            Howler.volume(player.isMuted ? 0 : 1);
        });

        $('.loop').on('click', function() {
            player.loopMode = (player.loopMode + 1) % 3;
            const icon = $(this).find('i');
            icon.removeClass('fa-refresh fa-repeat');

            if (player.loopMode === 1) { // Loop Playlist
                $(this).addClass('active').attr('title', 'Loop Playlist (Autoplay Next)');
                icon.addClass('fa-refresh');
            } else if (player.loopMode === 2) { // Loop Single
                $(this).addClass('active').attr('title', 'Loop Single Song');
                icon.addClass('fa-repeat');
            } else { // Loop Off
                $(this).removeClass('active').attr('title', 'Loop Off');
                icon.addClass('fa-refresh');
            }
            if (player.currentSound) player.currentSound.loop(player.loopMode === 2);
        });

        $('.random').on('click', function() { $(this).toggleClass('active'); player.isRandom = $(this).hasClass('active'); });

        $('.list').on('click', 'li', function() {
            if ($(this).data('isSorting')) return;
            loadTrack(parseInt($(this).attr('id').replace('track-', '')), true);
        });

        // --- Settings Handler ---
        const settingsCheckbox = $('#trix-show-on-all-sites');
        settingsCheckbox.prop('checked', GM_getValue('trixShowOnAllSites', true));
        settingsCheckbox.on('change', function() {
            GM_setValue('trixShowOnAllSites', $(this).prop('checked'));
            alert('TrixMusic settings saved. Please reload the page for changes to take full effect.');
        });

        // --- Draggable Logic ---
        container.draggable({ handle: ".player-main", containment: "window" });
        $('.list').sortable({
            revert: true,
            items: "> li",
            start: function(event, ui) { ui.item.data('isSorting', true); },
            stop: function(event, ui) { setTimeout(() => { ui.item.data('isSorting', false); }, 100); }
        }).disableSelection();

        $('#trixmusic-toggle-button').draggable({
            containment: "window",
            start: function() { $(this).data('isDragging', true); },
            stop: function() { setTimeout(() => { $(this).data('isDragging', false); }, 100); }
        }).on('click', function() {
            if (!$(this).data('isDragging')) { container.toggle(); }
        });

        // --- INITIATE UPDATE CHECK ---
        checkForUpdates();
    });
})();