添加循环播放功能
This commit is contained in:
parent
1bf4e07e88
commit
98620ae5d5
@ -25,6 +25,7 @@ dependencies {
|
||||
include modApi("me.shedaniel.cloth:cloth-config-fabric:17.0.142") {
|
||||
exclude(group: "net.fabricmc.fabric-api")
|
||||
}
|
||||
modImplementation "me.shedaniel.cloth:cloth-config-fabric:17.0.142"
|
||||
|
||||
// modCompileOnly("com.terraformersmc:modmenu:13.0.3")、
|
||||
modCompileOnly files("libs/modmenu-13.0.3.jar")
|
||||
|
@ -6,7 +6,7 @@ minecraft_version=1.21.4
|
||||
yarn_mappings=1.21.4+build.8
|
||||
loader_version=0.16.10
|
||||
# Mod Properties
|
||||
mod_version=1.14.514.013
|
||||
mod_version=1.14.514.019
|
||||
maven_group=semmiedev
|
||||
archives_base_name=disc_jockey
|
||||
# Dependencies
|
||||
|
@ -35,12 +35,12 @@ public class Main implements ClientModInitializer {
|
||||
public static final SongPlayer SONG_PLAYER = new SongPlayer();
|
||||
|
||||
public static File songsFolder;
|
||||
public static Config config;
|
||||
public static ConfigHolder<Config> configHolder;
|
||||
public static ModConfig config;
|
||||
public static ConfigHolder<ModConfig> configHolder;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
configHolder = AutoConfig.register(Config.class, JanksonConfigSerializer::new);
|
||||
configHolder = AutoConfig.register(ModConfig.class, JanksonConfigSerializer::new);
|
||||
config = configHolder.getConfig();
|
||||
|
||||
songsFolder = new File(FabricLoader.getInstance().getConfigDir()+File.separator+MOD_ID+File.separator+"songs");
|
||||
|
@ -7,11 +7,17 @@ import java.util.ArrayList;
|
||||
|
||||
@me.shedaniel.autoconfig.annotation.Config(name = Main.MOD_ID)
|
||||
@me.shedaniel.autoconfig.annotation.Config.Gui.Background("textures/block/note_block.png")
|
||||
public class Config implements ConfigData {
|
||||
public class ModConfig implements ConfigData {
|
||||
public boolean hideWarning;
|
||||
@ConfigEntry.Gui.Tooltip(count = 2) public boolean disableAsyncPlayback;
|
||||
@ConfigEntry.Gui.Tooltip(count = 2) public boolean omnidirectionalNoteBlockSounds = true;
|
||||
|
||||
@ConfigEntry.Gui.Excluded
|
||||
public String currentFolderPath = "";
|
||||
|
||||
@ConfigEntry.Gui.Excluded
|
||||
public SongPlayer.PlayMode playMode = SongPlayer.PlayMode.STOP_AFTER;
|
||||
|
||||
public enum ExpectedServerVersion {
|
||||
All,
|
||||
v1_20_4_Or_Earlier,
|
@ -7,6 +7,6 @@ import me.shedaniel.autoconfig.AutoConfig;
|
||||
public class ModMenuIntegration implements ModMenuApi {
|
||||
@Override
|
||||
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
||||
return parent -> AutoConfig.getConfigScreen(Config.class, parent).get();
|
||||
return parent -> AutoConfig.getConfigScreen(ModConfig.class, parent).get();
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public record Note(NoteBlockInstrument instrument, byte note) {
|
||||
NoteBlockInstrument.DIDGERIDOO,
|
||||
NoteBlockInstrument.BIT,
|
||||
NoteBlockInstrument.BANJO,
|
||||
NoteBlockInstrument.PLING
|
||||
NoteBlockInstrument.PLING,
|
||||
|
||||
};
|
||||
|
||||
|
@ -10,10 +10,11 @@ public class Song {
|
||||
public long[] notes = new long[0];
|
||||
|
||||
public short length, height, tempo, loopStartTick;
|
||||
public String fileName, name, author, originalAuthor, description, displayName;
|
||||
public String fileName, filePath, name, author, originalAuthor, description, displayName;
|
||||
public byte autoSaving, autoSavingDuration, timeSignature, vanillaInstrumentCount, formatVersion, loop, maxLoopCount;
|
||||
public int minutesSpent, leftClicks, rightClicks, blocksAdded, blocksRemoved;
|
||||
public String importFileName;
|
||||
public SongLoader.SongFolder folder;
|
||||
|
||||
public SongListWidget.SongEntry entry;
|
||||
public String searchableFileName, searchableName;
|
||||
@ -24,8 +25,7 @@ public class Song {
|
||||
}
|
||||
|
||||
public double millisecondsToTicks(long milliseconds) {
|
||||
// From NBS Format: The tempo of the song multiplied by 100 (for example, 1225 instead of 12.25). Measured in ticks per second.
|
||||
double songSpeed = (tempo / 100.0) / 20.0; // 20 Ticks per second (temp / 100 = 20) would be 1x speed
|
||||
double songSpeed = (tempo / 100.0) / 20.0;
|
||||
double oneMsTo20TickFraction = 1.0 / 50.0;
|
||||
return milliseconds * oneMsTo20TickFraction * songSpeed;
|
||||
}
|
||||
@ -39,5 +39,4 @@ public class Song {
|
||||
public double getLengthInSeconds() {
|
||||
return ticksToMilliseconds(length) / 1000.0;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,29 +11,42 @@ import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SongLoader {
|
||||
public static final ArrayList<Song> SONGS = new ArrayList<>();
|
||||
public static final ArrayList<SongFolder> FOLDERS = new ArrayList<>();
|
||||
public static final ArrayList<String> SONG_SUGGESTIONS = new ArrayList<>();
|
||||
public static volatile boolean loadingSongs;
|
||||
public static volatile boolean showToast;
|
||||
public static SongFolder currentFolder = null;
|
||||
|
||||
public static class SongFolder {
|
||||
public final String name;
|
||||
public final String path;
|
||||
public final ArrayList<Song> songs = new ArrayList<>();
|
||||
public final ArrayList<SongFolder> subFolders = new ArrayList<>();
|
||||
public SongListWidget.FolderEntry entry;
|
||||
|
||||
public SongFolder(String name, String path) {
|
||||
this.name = name;
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadSongs() {
|
||||
if (loadingSongs) return;
|
||||
new Thread(() -> {
|
||||
loadingSongs = true;
|
||||
SONGS.clear();
|
||||
FOLDERS.clear();
|
||||
SONG_SUGGESTIONS.clear();
|
||||
SONG_SUGGESTIONS.add("Songs are loading, please wait");
|
||||
for (File file : Main.songsFolder.listFiles()) {
|
||||
Song song = null;
|
||||
try {
|
||||
song = loadSong(file);
|
||||
} catch (Exception exception) {
|
||||
Main.LOGGER.error("Unable to read or parse song {}", file.getName(), exception);
|
||||
}
|
||||
if (song != null) SONGS.add(song);
|
||||
}
|
||||
|
||||
// Load root folder
|
||||
loadFolder(Main.songsFolder, null);
|
||||
|
||||
for (Song song : SONGS) SONG_SUGGESTIONS.add(song.displayName);
|
||||
Main.config.favorites.removeIf(favorite -> SongLoader.SONGS.stream().map(song -> song.fileName).noneMatch(favorite::equals));
|
||||
|
||||
@ -43,12 +56,44 @@ public class SongLoader {
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static void loadFolder(File folder, SongFolder parentFolder) {
|
||||
if (!folder.isDirectory()) return;
|
||||
|
||||
SongFolder songFolder = new SongFolder(folder.getName(), folder.getPath());
|
||||
if (parentFolder == null) {
|
||||
FOLDERS.add(songFolder);
|
||||
} else {
|
||||
parentFolder.subFolders.add(songFolder);
|
||||
}
|
||||
|
||||
File[] files = folder.listFiles();
|
||||
if (files == null) return;
|
||||
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
loadFolder(file, songFolder);
|
||||
} else {
|
||||
try {
|
||||
Song song = loadSong(file);
|
||||
if (song != null) {
|
||||
SONGS.add(song);
|
||||
songFolder.songs.add(song);
|
||||
song.folder = songFolder;
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Main.LOGGER.error("Unable to read or parse song {}", file.getName(), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Song loadSong(File file) throws IOException {
|
||||
if (file.isFile()) {
|
||||
BinaryReader reader = new BinaryReader(Files.newInputStream(file.toPath()));
|
||||
Song song = new Song();
|
||||
|
||||
song.fileName = file.getName().replaceAll("[\\n\\r]", "");
|
||||
song.filePath = file.getPath();
|
||||
|
||||
song.length = reader.readShort();
|
||||
|
||||
@ -99,7 +144,6 @@ public class SongLoader {
|
||||
byte noteId = (byte)(reader.readByte() - 33);
|
||||
|
||||
if (newFormat) {
|
||||
// Data that is not needed as it only works with commands
|
||||
reader.readByte(); // Velocity
|
||||
reader.readByte(); // Panning
|
||||
reader.readShort(); // Pitch
|
||||
@ -126,5 +170,10 @@ public class SongLoader {
|
||||
|
||||
public static void sort() {
|
||||
SONGS.sort(Comparator.comparing(song -> song.displayName));
|
||||
FOLDERS.sort(Comparator.comparing(folder -> folder.name));
|
||||
for (SongFolder folder : FOLDERS) {
|
||||
folder.songs.sort(Comparator.comparing(song -> song.displayName));
|
||||
folder.subFolders.sort(Comparator.comparing(subFolder -> subFolder.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,17 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
||||
this.playbackThread.start();
|
||||
}
|
||||
|
||||
public enum PlayMode {
|
||||
SINGLE_LOOP, // 单曲循环
|
||||
LIST_LOOP, // 列表循环
|
||||
RANDOM, // 随机播放
|
||||
STOP_AFTER // 播完就停
|
||||
}
|
||||
|
||||
private PlayMode playMode = PlayMode.STOP_AFTER;
|
||||
private boolean isRandomPlaying = false;
|
||||
private int randomIndex = -1;
|
||||
|
||||
public synchronized void stopPlaybackThread() {
|
||||
this.playbackThread = null; // Should stop on its own then
|
||||
}
|
||||
@ -103,8 +114,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
||||
}
|
||||
if (running) stop();
|
||||
this.song = song;
|
||||
//Main.LOGGER.info("Song length: " + song.length + " and tempo " + song.tempo);
|
||||
//Main.TICK_LISTENERS.add(this);
|
||||
this.loopSong = playMode == PlayMode.SINGLE_LOOP;
|
||||
if(this.playbackThread == null) startPlaybackThread();
|
||||
running = true;
|
||||
lastPlaybackTickAt = System.currentTimeMillis();
|
||||
@ -116,6 +126,18 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
||||
lastSwingSentAt = -1L;
|
||||
missingInstrumentBlocks.clear();
|
||||
didSongReachEnd = false;
|
||||
isRandomPlaying = playMode == PlayMode.RANDOM;
|
||||
if (isRandomPlaying) {
|
||||
randomIndex = getRandomSongIndex();
|
||||
}
|
||||
}
|
||||
|
||||
private int getRandomSongIndex() {
|
||||
if (SongLoader.currentFolder == null) {
|
||||
return (int) (Math.random() * SongLoader.SONGS.size());
|
||||
} else {
|
||||
return (int) (Math.random() * SongLoader.currentFolder.songs.size());
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
@ -215,8 +237,10 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
||||
if (index >= song.notes.length) {
|
||||
stop();
|
||||
didSongReachEnd = true;
|
||||
if(loopSong) {
|
||||
if (playMode == PlayMode.SINGLE_LOOP) {
|
||||
start(song);
|
||||
} else if (playMode == PlayMode.LIST_LOOP || playMode == PlayMode.RANDOM) {
|
||||
playNextSong();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -232,6 +256,30 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
||||
}
|
||||
}
|
||||
|
||||
private void playNextSong() {
|
||||
if (SongLoader.currentFolder == null || SongLoader.currentFolder.songs.isEmpty()) return;
|
||||
|
||||
int currentIndex = SongLoader.currentFolder.songs.indexOf(song);
|
||||
if (currentIndex == -1) return;
|
||||
|
||||
if (playMode == PlayMode.RANDOM) {
|
||||
int newIndex;
|
||||
do {
|
||||
newIndex = (int) (Math.random() * SongLoader.currentFolder.songs.size());
|
||||
} while (newIndex == currentIndex && SongLoader.currentFolder.songs.size() > 1);
|
||||
start(SongLoader.currentFolder.songs.get(newIndex));
|
||||
} else if (playMode == PlayMode.LIST_LOOP) {
|
||||
int nextIndex = (currentIndex + 1) % SongLoader.currentFolder.songs.size();
|
||||
start(SongLoader.currentFolder.songs.get(nextIndex));
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void setPlayMode(PlayMode mode) {
|
||||
this.playMode = mode;
|
||||
this.loopSong = mode == PlayMode.SINGLE_LOOP;
|
||||
}
|
||||
|
||||
// this is the original author‘s comment, i dont wanna delete it
|
||||
// TODO: 6/2/2022 Play note blocks every song tick, instead of every tick. That way the song will sound better
|
||||
// 11/1/2023 Playback now done in separate thread. Not ideal but better especially when FPS are low.
|
||||
@Override
|
||||
@ -260,11 +308,11 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
||||
final Vec3d playerEyePos = player.getEyePos();
|
||||
|
||||
final int maxOffset; // Rough estimates, of which blocks could be in reach
|
||||
if(Main.config.expectedServerVersion == Config.ExpectedServerVersion.v1_20_4_Or_Earlier) {
|
||||
if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_4_Or_Earlier) {
|
||||
maxOffset = 7;
|
||||
}else if(Main.config.expectedServerVersion == Config.ExpectedServerVersion.v1_20_5_Or_Later) {
|
||||
}else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_5_Or_Later) {
|
||||
maxOffset = (int) Math.ceil(player.getBlockInteractionRange() + 1.0 + 1.0);
|
||||
}else if(Main.config.expectedServerVersion == Config.ExpectedServerVersion.All) {
|
||||
}else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.All) {
|
||||
maxOffset = Math.min(7, (int) Math.ceil(player.getBlockInteractionRange() + 1.0 + 1.0));
|
||||
}else {
|
||||
throw new NotImplementedException("ExpectedServerVersion Value not implemented: " + Main.config.expectedServerVersion.name());
|
||||
@ -494,12 +542,12 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
||||
// (max distance is BlockInteractRange + 1.0).
|
||||
private boolean canInteractWith(ClientPlayerEntity player, BlockPos blockPos) {
|
||||
final Vec3d eyePos = player.getEyePos();
|
||||
if(Main.config.expectedServerVersion == Config.ExpectedServerVersion.v1_20_4_Or_Earlier) {
|
||||
if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_4_Or_Earlier) {
|
||||
return eyePos.squaredDistanceTo(blockPos.toCenterPos()) <= 6.0 * 6.0;
|
||||
}else if(Main.config.expectedServerVersion == Config.ExpectedServerVersion.v1_20_5_Or_Later) {
|
||||
}else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_5_Or_Later) {
|
||||
double blockInteractRange = player.getBlockInteractionRange() + 1.0;
|
||||
return new Box(blockPos).squaredMagnitude(eyePos) < blockInteractRange * blockInteractRange;
|
||||
}else if(Main.config.expectedServerVersion == Config.ExpectedServerVersion.All) {
|
||||
}else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.All) {
|
||||
// Require both checks to succeed (aka use worst distance)
|
||||
double blockInteractRange = player.getBlockInteractionRange() + 1.0;
|
||||
return eyePos.squaredDistanceTo(blockPos.toCenterPos()) <= 6.0 * 6.0
|
||||
|
@ -3,6 +3,7 @@ package semmiedev.disc_jockey.gui;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
|
||||
import net.minecraft.client.gui.widget.EntryListWidget;
|
||||
import net.minecraft.text.Text;
|
||||
@ -10,50 +11,44 @@ import net.minecraft.util.Identifier;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import semmiedev.disc_jockey.Main;
|
||||
import semmiedev.disc_jockey.Song;
|
||||
import semmiedev.disc_jockey.SongLoader;
|
||||
import semmiedev.disc_jockey.SongLoader.SongFolder;
|
||||
import semmiedev.disc_jockey.gui.screen.DiscJockeyScreen;
|
||||
|
||||
public class SongListWidget extends EntryListWidget<SongListWidget.SongEntry> {
|
||||
public class SongListWidget extends EntryListWidget<SongListWidget.Entry> {
|
||||
private static final String FAVORITE_EMOJI = "收藏★";
|
||||
private static final String NOT_FAVORITE_EMOJI = "收藏☆";
|
||||
private static final String FOLDER_EMOJI = "📁";
|
||||
private static final int LEFT_PADDING = 10;
|
||||
private final Screen parentScreen;
|
||||
|
||||
public static abstract class Entry extends EntryListWidget.Entry<Entry> {
|
||||
public abstract boolean isSelected();
|
||||
public abstract void setSelected(boolean selected);
|
||||
public abstract boolean isSongEntry();
|
||||
|
||||
public SongListWidget(MinecraftClient client, int width, int height, int top, int itemHeight) {
|
||||
super(client, width, height, top, itemHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowWidth() {
|
||||
return width - 40;
|
||||
@Nullable
|
||||
public SongEntry getSelectedSongOrNull() {
|
||||
Entry selected = getSelectedOrNull();
|
||||
return selected instanceof SongEntry ? (SongEntry) selected : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getScrollbarX() {
|
||||
return width - 12;
|
||||
@Nullable
|
||||
public FolderEntry getSelectedFolderOrNull() {
|
||||
Entry selected = getSelectedOrNull();
|
||||
return selected instanceof FolderEntry ? (FolderEntry) selected : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(@Nullable SongListWidget.SongEntry entry) {
|
||||
SongListWidget.SongEntry selectedEntry = getSelectedOrNull();
|
||||
if (selectedEntry != null) selectedEntry.selected = false;
|
||||
if (entry != null) entry.selected = true;
|
||||
super.setSelected(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendClickableNarrations(NarrationMessageBuilder builder) {
|
||||
// Who cares
|
||||
}
|
||||
|
||||
// TODO: 6/2/2022 Add a delete icon
|
||||
public static class SongEntry extends Entry<SongEntry> {
|
||||
public static class SongEntry extends Entry {
|
||||
private static final Identifier ICONS = Identifier.of(Main.MOD_ID, "textures/gui/icons.png");
|
||||
|
||||
public final int index;
|
||||
public final Song song;
|
||||
|
||||
public boolean selected, favorite;
|
||||
public SongListWidget songListWidget;
|
||||
|
||||
private final MinecraftClient client = MinecraftClient.getInstance();
|
||||
|
||||
private int x, y, entryWidth, entryHeight;
|
||||
|
||||
public SongEntry(Song song, int index) {
|
||||
@ -70,15 +65,26 @@ public class SongListWidget extends EntryListWidget<SongListWidget.SongEntry> {
|
||||
context.fill(x + 1, y + 1, x + entryWidth - 1, y + entryHeight - 1, 0x000000);
|
||||
}
|
||||
|
||||
context.drawCenteredTextWithShadow(client.textRenderer, song.displayName, x + entryWidth / 2, y + 5, selected ? 0xFFFFFF : 0x808080);
|
||||
|
||||
// 收藏图标
|
||||
String emoji = String.valueOf(favorite ? FAVORITE_EMOJI : NOT_FAVORITE_EMOJI);
|
||||
int emojiWidth = client.textRenderer.getWidth(emoji);
|
||||
context.drawTextWithShadow(
|
||||
client.textRenderer,
|
||||
emoji,
|
||||
x + 2, y + 2,
|
||||
x + 4, y + 6,
|
||||
favorite ? 0xFFD700 : 0x808080
|
||||
);
|
||||
|
||||
// 歌曲名称靠左显示,从收藏图标右侧开始
|
||||
int textX = x + emojiWidth + 8;
|
||||
int maxWidth = entryWidth - emojiWidth - 12;
|
||||
String displayText = client.textRenderer.trimToWidth(song.displayName, maxWidth);
|
||||
context.drawTextWithShadow(
|
||||
client.textRenderer,
|
||||
displayText,
|
||||
textX, y + 6,
|
||||
selected ? 0xFFFFFF : 0x808080
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -97,12 +103,159 @@ public class SongListWidget extends EntryListWidget<SongListWidget.SongEntry> {
|
||||
}
|
||||
|
||||
private boolean isOverFavoriteButton(double mouseX, double mouseY) {
|
||||
int textWidth = client.textRenderer.getWidth(favorite ? FAVORITE_EMOJI : NOT_FAVORITE_EMOJI);
|
||||
int textHeight = 8;
|
||||
return mouseX > x + 2 &&
|
||||
mouseX < x + 2 + textWidth &&
|
||||
mouseY > y + 2 &&
|
||||
mouseY < y + 2 + textHeight;
|
||||
int iconX = x + 2;
|
||||
int iconWidth = client.textRenderer.getWidth(favorite ? FAVORITE_EMOJI : NOT_FAVORITE_EMOJI);
|
||||
int iconHeight = 8;
|
||||
|
||||
return mouseX >= iconX &&
|
||||
mouseX <= iconX + iconWidth &&
|
||||
mouseY >= y + 6 &&
|
||||
mouseY <= y + 6 + iconHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSongEntry() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FolderEntry extends Entry {
|
||||
public SongFolder folder;
|
||||
public boolean selected;
|
||||
public SongListWidget songListWidget;
|
||||
public String displayName;
|
||||
|
||||
private final MinecraftClient client = MinecraftClient.getInstance();
|
||||
private int x, y, entryWidth, entryHeight;
|
||||
|
||||
public FolderEntry(@Nullable SongFolder folder, SongListWidget songListWidget) {
|
||||
this.folder = folder;
|
||||
this.songListWidget = songListWidget;
|
||||
this.displayName = folder != null ? folder.name : "..";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
|
||||
this.x = x; this.y = y; this.entryWidth = entryWidth; this.entryHeight = entryHeight;
|
||||
|
||||
if (selected) {
|
||||
context.fill(x, y, x + entryWidth, y + entryHeight, 0xFFFFFF);
|
||||
context.fill(x + 1, y + 1, x + entryWidth - 1, y + entryHeight - 1, 0x000000);
|
||||
}
|
||||
|
||||
// 文件夹图标和名称靠左显示
|
||||
String displayText = FOLDER_EMOJI + " " + displayName;
|
||||
int maxWidth = entryWidth - 8;
|
||||
displayText = client.textRenderer.trimToWidth(displayText, maxWidth);
|
||||
context.drawTextWithShadow(
|
||||
client.textRenderer,
|
||||
displayText,
|
||||
x + 6, y + 6,
|
||||
selected ? 0xFFFFFF : 0x808080
|
||||
);
|
||||
}
|
||||
|
||||
private SongFolder findParentFolder(SongFolder current) {
|
||||
if (current == null) return null;
|
||||
|
||||
for (SongFolder folder : SongLoader.FOLDERS) {
|
||||
if (folder.subFolders.contains(current)) {
|
||||
return folder;
|
||||
}
|
||||
for (SongFolder subFolder : folder.subFolders) {
|
||||
if (subFolder.subFolders.contains(current)) {
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (button == 0) {
|
||||
if (songListWidget.getParentScreen() instanceof DiscJockeyScreen screen) {
|
||||
if (this.folder == null) {
|
||||
SongFolder parent = screen.findParentFolder(SongLoader.currentFolder);
|
||||
if (parent != null || SongLoader.FOLDERS.contains(SongLoader.currentFolder)) {
|
||||
screen.currentFolder = parent;
|
||||
SongLoader.currentFolder = parent;
|
||||
screen.shouldFilter = true;
|
||||
}
|
||||
} else {
|
||||
screen.currentFolder = this.folder;
|
||||
SongLoader.currentFolder = this.folder;
|
||||
screen.shouldFilter = true;
|
||||
}
|
||||
songListWidget.setSelected(this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSongEntry() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public SongListWidget(MinecraftClient client, int width, int height, int top, int itemHeight, Screen parentScreen) {
|
||||
super(client, width, height, top, itemHeight);
|
||||
this.parentScreen = parentScreen;
|
||||
}
|
||||
|
||||
public Screen getParentScreen() {
|
||||
return parentScreen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowLeft() {
|
||||
return super.getRowLeft() + LEFT_PADDING;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public int getRowWidth() {
|
||||
return width - 40;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getScrollbarX() {
|
||||
return width - 12;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(@Nullable Entry entry) {
|
||||
Entry selectedEntry = getSelectedOrNull();
|
||||
if (selectedEntry != null) selectedEntry.setSelected(false);
|
||||
if (entry != null) entry.setSelected(true);
|
||||
super.setSelected(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendClickableNarrations(NarrationMessageBuilder builder) {
|
||||
// Who cares
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,9 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import semmiedev.disc_jockey.SongLoader.SongFolder;
|
||||
import semmiedev.disc_jockey.SongPlayer.PlayMode;
|
||||
|
||||
public class DiscJockeyScreen extends Screen {
|
||||
private static final MutableText
|
||||
SELECT_SONG = Text.translatable(Main.MOD_ID+".screen.select_song"),
|
||||
@ -36,9 +39,26 @@ public class DiscJockeyScreen extends Screen {
|
||||
|
||||
private SongListWidget songListWidget;
|
||||
private ButtonWidget playButton, previewButton;
|
||||
private boolean shouldFilter;
|
||||
public boolean shouldFilter;
|
||||
private String query = "";
|
||||
|
||||
private static final MutableText
|
||||
FOLDER_UP = Text.literal("↑"),
|
||||
CURRENT_FOLDER = Text.translatable(Main.MOD_ID+".screen.current_folder"),
|
||||
PLAY_MODE = Text.translatable(Main.MOD_ID+".screen.play_mode"),
|
||||
MODE_SINGLE = Text.translatable(Main.MOD_ID+".screen.mode_single"),
|
||||
MODE_LIST = Text.translatable(Main.MOD_ID+".screen.mode_list"),
|
||||
MODE_RANDOM = Text.translatable(Main.MOD_ID+".screen.mode_random"),
|
||||
MODE_STOP = Text.translatable(Main.MOD_ID+".screen.mode_stop");
|
||||
|
||||
private static final MutableText
|
||||
OPEN_FOLDER = Text.translatable(Main.MOD_ID+".screen.open_folder"),
|
||||
RELOAD = Text.translatable(Main.MOD_ID+".screen.reload");
|
||||
|
||||
private ButtonWidget folderUpButton, playModeButton;
|
||||
public SongFolder currentFolder;
|
||||
private PlayMode currentPlayMode = PlayMode.STOP_AFTER;
|
||||
|
||||
public DiscJockeyScreen() {
|
||||
super(Main.NAME);
|
||||
}
|
||||
@ -46,19 +66,56 @@ public class DiscJockeyScreen extends Screen {
|
||||
@Override
|
||||
protected void init() {
|
||||
shouldFilter = true;
|
||||
songListWidget = new SongListWidget(client, width, height - 64 - 32, 32, 20);
|
||||
songListWidget = new SongListWidget(client, width, height - 64 - 32, 32, 20, this);
|
||||
|
||||
// 恢复播放模式
|
||||
currentPlayMode = Main.config.playMode;
|
||||
|
||||
// 恢复文件夹状态
|
||||
if (!Main.config.currentFolderPath.isEmpty()) {
|
||||
currentFolder = findFolderByPath(Main.config.currentFolderPath);
|
||||
SongLoader.currentFolder = currentFolder;
|
||||
}
|
||||
|
||||
addDrawableChild(songListWidget);
|
||||
for (int i = 0; i < SongLoader.SONGS.size(); i++) {
|
||||
Song song = SongLoader.SONGS.get(i);
|
||||
song.entry.songListWidget = songListWidget;
|
||||
if (song.entry.selected) songListWidget.setSelected(song.entry);
|
||||
|
||||
// 添加文件夹条目
|
||||
if (song.folder != null && !songListWidget.children().contains(song.folder.entry)) {
|
||||
song.folder.entry = new SongListWidget.FolderEntry(song.folder, songListWidget);
|
||||
songListWidget.children().add(song.folder.entry);
|
||||
}
|
||||
}
|
||||
|
||||
folderUpButton = ButtonWidget.builder(FOLDER_UP, button -> {
|
||||
if (currentFolder != null) {
|
||||
currentFolder = null;
|
||||
SongLoader.currentFolder = null;
|
||||
shouldFilter = true;
|
||||
}
|
||||
}).dimensions(10, 10, 20, 20).build();
|
||||
addDrawableChild(folderUpButton);
|
||||
|
||||
playModeButton = ButtonWidget.builder(getPlayModeText(), button -> {
|
||||
switch (currentPlayMode) {
|
||||
case SINGLE_LOOP -> currentPlayMode = PlayMode.LIST_LOOP;
|
||||
case LIST_LOOP -> currentPlayMode = PlayMode.RANDOM;
|
||||
case RANDOM -> currentPlayMode = PlayMode.STOP_AFTER;
|
||||
case STOP_AFTER -> currentPlayMode = PlayMode.SINGLE_LOOP;
|
||||
}
|
||||
Main.SONG_PLAYER.setPlayMode(currentPlayMode);
|
||||
playModeButton.setMessage(getPlayModeText());
|
||||
}).dimensions(width - 120, 10, 100, 20).build();
|
||||
addDrawableChild(playModeButton);
|
||||
|
||||
playButton = ButtonWidget.builder(PLAY, button -> {
|
||||
if (Main.SONG_PLAYER.running) {
|
||||
Main.SONG_PLAYER.stop();
|
||||
} else {
|
||||
SongListWidget.SongEntry entry = songListWidget.getSelectedOrNull();
|
||||
SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull();
|
||||
if (entry != null) {
|
||||
Main.SONG_PLAYER.start(entry.song);
|
||||
client.setScreen(null);
|
||||
@ -71,7 +128,7 @@ public class DiscJockeyScreen extends Screen {
|
||||
if (Main.PREVIEWER.running) {
|
||||
Main.PREVIEWER.stop();
|
||||
} else {
|
||||
SongListWidget.SongEntry entry = songListWidget.getSelectedOrNull();
|
||||
SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull();
|
||||
if (entry != null) Main.PREVIEWER.start(entry.song);
|
||||
}
|
||||
}).dimensions(width / 2 - 50, height - 61, 100, 20).build();
|
||||
@ -80,7 +137,7 @@ public class DiscJockeyScreen extends Screen {
|
||||
addDrawableChild(ButtonWidget.builder(Text.translatable(Main.MOD_ID+".screen.blocks"), button -> {
|
||||
// TODO: 6/2/2022 Add an auto build mode
|
||||
if (BlocksOverlay.itemStacks == null) {
|
||||
SongListWidget.SongEntry entry = songListWidget.getSelectedOrNull();
|
||||
SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull();
|
||||
if (entry != null) {
|
||||
client.setScreen(null);
|
||||
|
||||
@ -116,7 +173,43 @@ public class DiscJockeyScreen extends Screen {
|
||||
}
|
||||
}).dimensions(width / 2 + 60, height - 61, 100, 20).build());
|
||||
|
||||
TextFieldWidget searchBar = new TextFieldWidget(textRenderer, width / 2 - 75, height - 31, 150, 20, Text.translatable(Main.MOD_ID+".screen.search"));
|
||||
// 打开文件夹
|
||||
addDrawableChild(ButtonWidget.builder(OPEN_FOLDER, button -> {
|
||||
try {
|
||||
String folderPath = currentFolder != null ?
|
||||
currentFolder.path :
|
||||
Main.songsFolder.getAbsolutePath(); // 使用绝对路径
|
||||
|
||||
File target = new File(folderPath);
|
||||
if (!target.exists()) {
|
||||
client.inGameHud.getChatHud().addMessage(
|
||||
Text.translatable(Main.MOD_ID+".screen.folder_not_exist", folderPath)
|
||||
.formatted(Formatting.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
// Windows 专用命令,其他的不会
|
||||
if (System.getProperty("os.name").toLowerCase().contains("win")) {
|
||||
new ProcessBuilder("explorer.exe", "/select,", target.getAbsolutePath()).start();
|
||||
} else {
|
||||
java.awt.Desktop.getDesktop().open(target);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Main.LOGGER.error("打开文件夹失败", e);
|
||||
client.inGameHud.getChatHud().addMessage(
|
||||
Text.translatable(Main.MOD_ID+".screen.open_folder_failed")
|
||||
.formatted(Formatting.RED));
|
||||
}
|
||||
}).dimensions(width / 2 - 160, height - 31, 100, 20).build());
|
||||
|
||||
|
||||
// 重新加载
|
||||
addDrawableChild(ButtonWidget.builder(RELOAD, button -> {
|
||||
SongLoader.loadSongs();
|
||||
client.setScreen(null);
|
||||
}).dimensions(width / 2 + 60, height - 31, 100, 20).build());
|
||||
|
||||
TextFieldWidget searchBar = new TextFieldWidget(textRenderer, width / 2 - 50, height - 31, 100, 20, Text.translatable(Main.MOD_ID+".screen.search"));
|
||||
searchBar.setChangedListener(query -> {
|
||||
query = query.toLowerCase().replaceAll("\\s", "");
|
||||
if (this.query.equals(query)) return;
|
||||
@ -134,6 +227,11 @@ public class DiscJockeyScreen extends Screen {
|
||||
|
||||
context.drawCenteredTextWithShadow(textRenderer, DROP_HINT, width / 2, 5, 0xFFFFFF);
|
||||
context.drawCenteredTextWithShadow(textRenderer, SELECT_SONG, width / 2, 20, 0xFFFFFF);
|
||||
|
||||
// 显示当前文件夹和播放模式
|
||||
String folderName = currentFolder == null ? "/" : currentFolder.name;
|
||||
context.drawTextWithShadow(textRenderer, CURRENT_FOLDER.getString() + ": " + folderName, 35, 15, 0xFFFFFF);
|
||||
context.drawTextWithShadow(textRenderer, PLAY_MODE.getString() + ":", width - 220, 15, 0xFFFFFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -143,15 +241,59 @@ public class DiscJockeyScreen extends Screen {
|
||||
|
||||
if (shouldFilter) {
|
||||
shouldFilter = false;
|
||||
// songListWidget.setScrollAmount(0);
|
||||
songListWidget.children().clear();
|
||||
boolean empty = query.isEmpty();
|
||||
int favoriteIndex = 0;
|
||||
for (Song song : SongLoader.SONGS) {
|
||||
if (empty || song.searchableFileName.contains(query) || song.searchableName.contains(query)) {
|
||||
if (song.entry.favorite) {
|
||||
songListWidget.children().add(favoriteIndex++, song.entry);
|
||||
|
||||
boolean isInSongsOrSubfolder = currentFolder == null ||
|
||||
currentFolder.path.startsWith(Main.songsFolder.getPath());
|
||||
|
||||
if (currentFolder == null) {
|
||||
for (SongFolder folder : SongLoader.FOLDERS) {
|
||||
if (empty || folder.name.toLowerCase().contains(query)) {
|
||||
if (folder.entry == null) {
|
||||
folder.entry = new SongListWidget.FolderEntry(folder, songListWidget);
|
||||
}
|
||||
songListWidget.children().add(folder.entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 返回上级
|
||||
SongListWidget.FolderEntry parentEntry = new SongListWidget.FolderEntry(null, songListWidget);
|
||||
parentEntry.displayName = "..";
|
||||
songListWidget.children().add(parentEntry);
|
||||
|
||||
// 子文件夹
|
||||
for (SongFolder subFolder : currentFolder.subFolders) {
|
||||
if (empty || subFolder.name.toLowerCase().contains(query)) {
|
||||
if (subFolder.entry == null) {
|
||||
subFolder.entry = new SongListWidget.FolderEntry(subFolder, songListWidget);
|
||||
}
|
||||
songListWidget.children().add(subFolder.entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在songs目录或其子目录中才显示歌曲(原作者的💩跑我这了)
|
||||
if (isInSongsOrSubfolder) {
|
||||
// 歌曲条目
|
||||
List<Song> songsToShow = currentFolder == null ?
|
||||
SongLoader.SONGS.stream()
|
||||
.filter(song -> song.folder == null)
|
||||
.collect(Collectors.toList()) :
|
||||
currentFolder.songs.stream()
|
||||
.filter(song -> song.folder == currentFolder)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 已收藏歌曲
|
||||
for (Song song : songsToShow) {
|
||||
if (song.entry.favorite && (empty || song.searchableFileName.contains(query) || song.searchableName.contains(query))) {
|
||||
songListWidget.children().add(song.entry);
|
||||
}
|
||||
}
|
||||
|
||||
// 未收藏歌曲
|
||||
for (Song song : songsToShow) {
|
||||
if (!song.entry.favorite && (empty || song.searchableFileName.contains(query) || song.searchableName.contains(query))) {
|
||||
songListWidget.children().add(song.entry);
|
||||
}
|
||||
}
|
||||
@ -159,6 +301,40 @@ public class DiscJockeyScreen extends Screen {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public SongFolder findParentFolder(SongFolder current) {
|
||||
if (current == null) return null;
|
||||
|
||||
if (SongLoader.FOLDERS.contains(current)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (SongFolder folder : SongLoader.FOLDERS) {
|
||||
if (folder.subFolders.contains(current)) {
|
||||
return folder;
|
||||
}
|
||||
SongFolder found = findParentInSubfolders(folder, current);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SongFolder findParentInSubfolders(SongFolder parent, SongFolder target) {
|
||||
for (SongFolder subFolder : parent.subFolders) {
|
||||
if (subFolder == target) {
|
||||
return parent;
|
||||
}
|
||||
SongFolder found = findParentInSubfolders(subFolder, target);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFilesDropped(List<Path> paths) {
|
||||
String string = paths.stream().map(Path::getFileName).map(Path::toString).collect(Collectors.joining(", "));
|
||||
@ -196,6 +372,62 @@ public class DiscJockeyScreen extends Screen {
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
// 保存当前文件夹
|
||||
Main.config.currentFolderPath = currentFolder != null ? currentFolder.path : "";
|
||||
// 保存播放模式
|
||||
Main.config.playMode = currentPlayMode;
|
||||
// 异步保存配置
|
||||
new Thread(() -> Main.configHolder.save()).start();
|
||||
}
|
||||
|
||||
private SongFolder findInSubFolders(SongFolder parent, SongFolder target) {
|
||||
for (SongFolder subFolder : parent.subFolders) {
|
||||
if (subFolder == target) {
|
||||
return parent;
|
||||
}
|
||||
SongFolder found = findInSubFolders(subFolder, target);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SongFolder findFolderByPath(String path) {
|
||||
for (SongFolder folder : SongLoader.FOLDERS) {
|
||||
if (folder.path.equals(path)) {
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
|
||||
for (SongFolder folder : SongLoader.FOLDERS) {
|
||||
SongFolder found = findFolderByPathInSubfolders(folder, path);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SongFolder findFolderByPathInSubfolders(SongFolder parent, String targetPath) {
|
||||
for (SongFolder subFolder : parent.subFolders) {
|
||||
if (subFolder.path.equals(targetPath)) {
|
||||
return subFolder;
|
||||
}
|
||||
SongFolder found = findFolderByPathInSubfolders(subFolder, targetPath);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Text getPlayModeText() {
|
||||
return switch (currentPlayMode) {
|
||||
case SINGLE_LOOP -> MODE_SINGLE;
|
||||
case LIST_LOOP -> MODE_LIST;
|
||||
case RANDOM -> MODE_RANDOM;
|
||||
case STOP_AFTER -> MODE_STOP;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@
|
||||
"disc_jockey.loop_status": "Loop song: %s",
|
||||
"disc_jockey.loop_enabled": "Enabled looping of current song.",
|
||||
"disc_jockey.loop_disabled": "Disabled looping of current song.",
|
||||
"disc_jockey.warning": "WARNING!!! This mod is very likely to get false flagged as hacks, please contact a server administrator before using this mod! (You can disable this warning in the mod settings)",
|
||||
"disc_jockey.warning": "WARNING!!! This mod is very likely to get false flagged as hacks, please contact a server administrator before using this mod! (You can disable this warning in the mod settings)\nThe current version is an unofficial version, revised by BRanulf\nFor learning reference only, please support the official, don't contact me XD",
|
||||
"key.category.disc_jockey": "Disc Jockey",
|
||||
"disc_jockey.key_bind.open_screen": "Open song selection screen",
|
||||
"text.autoconfig.disc_jockey.title": "Disc Jockey",
|
||||
@ -51,5 +51,15 @@
|
||||
"text.autoconfig.disc_jockey.option.expectedServerVersion.@Tooltip[2]": "Selecting the wrong version could cause you not to be able to play some distant note blocks which could break/worsen playback",
|
||||
"text.autoconfig.disc_jockey.option.expectedServerVersion.@Tooltip[3]": "If you're unsure, or play on many different server versions and don't mind not reaching every possible note block, select \"All\"",
|
||||
"text.autoconfig.disc_jockey.option.delayPlaybackStartBySecs": "Delay playback by (seconds)",
|
||||
"text.autoconfig.disc_jockey.option.delayPlaybackStartBySecs.@Tooltip": "Delays playback for specified seconds, after tuning finished, if any (e.g. 0.5 for half a second delay)."
|
||||
"text.autoconfig.disc_jockey.option.delayPlaybackStartBySecs.@Tooltip": "Delays playback for specified seconds, after tuning finished, if any (e.g. 0.5 for half a second delay).",
|
||||
"disc_jockey.screen.current_folder": "Current Folder",
|
||||
"disc_jockey.screen.play_mode": "Play Mode",
|
||||
"disc_jockey.screen.mode_single": "Single Loop",
|
||||
"disc_jockey.screen.mode_list": "List Loop",
|
||||
"disc_jockey.screen.mode_random": "Random",
|
||||
"disc_jockey.screen.mode_stop": "Stop After",
|
||||
"disc_jockey.screen.open_folder": "Open Folder",
|
||||
"disc_jockey.screen.open_folder_failed": "Failed to open folder",
|
||||
"disc_jockey.screen.reload": "Reload Songs",
|
||||
"disc_jockey.screen.reloading": "Reloading songs..."
|
||||
}
|
@ -34,7 +34,7 @@
|
||||
"disc_jockey.loop_status": "循环播放:%s",
|
||||
"disc_jockey.loop_enabled": "已启用当前歌曲循环播放",
|
||||
"disc_jockey.loop_disabled": "已禁用当前歌曲循环播放",
|
||||
"disc_jockey.warning": "警告!此模组极易被误判为作弊工具,使用前请联系服务器管理员!(可在模组设置中关闭此警告)\n当前版本:1.14.514(1.21.4)为非官方版本,由BRanulf改版,翻译也是这家伙提供的。\n仅供学习参考,请支持官方,别找我XD",
|
||||
"disc_jockey.warning": "警告!此模组极易被误判为作弊工具,使用前请联系服务器管理员!(可在模组设置中关闭此警告)\n当前版本为非官方版本,由BRanulf改版,翻译也是这家伙提供的。\n仅供学习参考,请支持官方,别找我XD",
|
||||
"key.category.disc_jockey": "Disc Jockey",
|
||||
"disc_jockey.key_bind.open_screen": "打开歌曲选择界面",
|
||||
"text.autoconfig.disc_jockey.title": "Disc Jockey",
|
||||
@ -51,5 +51,14 @@
|
||||
"text.autoconfig.disc_jockey.option.expectedServerVersion.@Tooltip[2]": "版本选择错误可能导致无法触发部分音符盒,影响播放效果",
|
||||
"text.autoconfig.disc_jockey.option.expectedServerVersion.@Tooltip[3]": "若不确认版本,或需兼容多版本服务器,请选择“全部”",
|
||||
"text.autoconfig.disc_jockey.option.delayPlaybackStartBySecs": "播放延迟(秒)",
|
||||
"text.autoconfig.disc_jockey.option.delayPlaybackStartBySecs.@Tooltip": "调音完成后延迟指定秒数再开始播放(如 0.5 表示延迟半秒)。"
|
||||
"text.autoconfig.disc_jockey.option.delayPlaybackStartBySecs.@Tooltip": "调音完成后延迟指定秒数再开始播放(如 0.5 表示延迟半秒)。",
|
||||
"disc_jockey.screen.current_folder": "当前文件夹",
|
||||
"disc_jockey.screen.play_mode": "播放模式",
|
||||
"disc_jockey.screen.mode_single": "单曲循环",
|
||||
"disc_jockey.screen.mode_list": "列表循环",
|
||||
"disc_jockey.screen.mode_random": "随机播放",
|
||||
"disc_jockey.screen.mode_stop": "播完停止","disc_jockey.screen.open_folder": "打开文件夹",
|
||||
"disc_jockey.screen.open_folder_failed": "无法打开文件夹",
|
||||
"disc_jockey.screen.reload": "重新加载",
|
||||
"disc_jockey.screen.reloading": "正在重新加载..."
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user