添加循环播放功能
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") {
|
include modApi("me.shedaniel.cloth:cloth-config-fabric:17.0.142") {
|
||||||
exclude(group: "net.fabricmc.fabric-api")
|
exclude(group: "net.fabricmc.fabric-api")
|
||||||
}
|
}
|
||||||
|
modImplementation "me.shedaniel.cloth:cloth-config-fabric:17.0.142"
|
||||||
|
|
||||||
// modCompileOnly("com.terraformersmc:modmenu:13.0.3")、
|
// modCompileOnly("com.terraformersmc:modmenu:13.0.3")、
|
||||||
modCompileOnly files("libs/modmenu-13.0.3.jar")
|
modCompileOnly files("libs/modmenu-13.0.3.jar")
|
||||||
|
@ -6,7 +6,7 @@ minecraft_version=1.21.4
|
|||||||
yarn_mappings=1.21.4+build.8
|
yarn_mappings=1.21.4+build.8
|
||||||
loader_version=0.16.10
|
loader_version=0.16.10
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=1.14.514.013
|
mod_version=1.14.514.019
|
||||||
maven_group=semmiedev
|
maven_group=semmiedev
|
||||||
archives_base_name=disc_jockey
|
archives_base_name=disc_jockey
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
@ -35,12 +35,12 @@ public class Main implements ClientModInitializer {
|
|||||||
public static final SongPlayer SONG_PLAYER = new SongPlayer();
|
public static final SongPlayer SONG_PLAYER = new SongPlayer();
|
||||||
|
|
||||||
public static File songsFolder;
|
public static File songsFolder;
|
||||||
public static Config config;
|
public static ModConfig config;
|
||||||
public static ConfigHolder<Config> configHolder;
|
public static ConfigHolder<ModConfig> configHolder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitializeClient() {
|
public void onInitializeClient() {
|
||||||
configHolder = AutoConfig.register(Config.class, JanksonConfigSerializer::new);
|
configHolder = AutoConfig.register(ModConfig.class, JanksonConfigSerializer::new);
|
||||||
config = configHolder.getConfig();
|
config = configHolder.getConfig();
|
||||||
|
|
||||||
songsFolder = new File(FabricLoader.getInstance().getConfigDir()+File.separator+MOD_ID+File.separator+"songs");
|
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(name = Main.MOD_ID)
|
||||||
@me.shedaniel.autoconfig.annotation.Config.Gui.Background("textures/block/note_block.png")
|
@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;
|
public boolean hideWarning;
|
||||||
@ConfigEntry.Gui.Tooltip(count = 2) public boolean disableAsyncPlayback;
|
@ConfigEntry.Gui.Tooltip(count = 2) public boolean disableAsyncPlayback;
|
||||||
@ConfigEntry.Gui.Tooltip(count = 2) public boolean omnidirectionalNoteBlockSounds = true;
|
@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 {
|
public enum ExpectedServerVersion {
|
||||||
All,
|
All,
|
||||||
v1_20_4_Or_Earlier,
|
v1_20_4_Or_Earlier,
|
@ -7,6 +7,6 @@ import me.shedaniel.autoconfig.AutoConfig;
|
|||||||
public class ModMenuIntegration implements ModMenuApi {
|
public class ModMenuIntegration implements ModMenuApi {
|
||||||
@Override
|
@Override
|
||||||
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
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.DIDGERIDOO,
|
||||||
NoteBlockInstrument.BIT,
|
NoteBlockInstrument.BIT,
|
||||||
NoteBlockInstrument.BANJO,
|
NoteBlockInstrument.BANJO,
|
||||||
NoteBlockInstrument.PLING
|
NoteBlockInstrument.PLING,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,10 +10,11 @@ public class Song {
|
|||||||
public long[] notes = new long[0];
|
public long[] notes = new long[0];
|
||||||
|
|
||||||
public short length, height, tempo, loopStartTick;
|
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 byte autoSaving, autoSavingDuration, timeSignature, vanillaInstrumentCount, formatVersion, loop, maxLoopCount;
|
||||||
public int minutesSpent, leftClicks, rightClicks, blocksAdded, blocksRemoved;
|
public int minutesSpent, leftClicks, rightClicks, blocksAdded, blocksRemoved;
|
||||||
public String importFileName;
|
public String importFileName;
|
||||||
|
public SongLoader.SongFolder folder;
|
||||||
|
|
||||||
public SongListWidget.SongEntry entry;
|
public SongListWidget.SongEntry entry;
|
||||||
public String searchableFileName, searchableName;
|
public String searchableFileName, searchableName;
|
||||||
@ -24,8 +25,7 @@ public class Song {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public double millisecondsToTicks(long milliseconds) {
|
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;
|
||||||
double songSpeed = (tempo / 100.0) / 20.0; // 20 Ticks per second (temp / 100 = 20) would be 1x speed
|
|
||||||
double oneMsTo20TickFraction = 1.0 / 50.0;
|
double oneMsTo20TickFraction = 1.0 / 50.0;
|
||||||
return milliseconds * oneMsTo20TickFraction * songSpeed;
|
return milliseconds * oneMsTo20TickFraction * songSpeed;
|
||||||
}
|
}
|
||||||
@ -39,5 +39,4 @@ public class Song {
|
|||||||
public double getLengthInSeconds() {
|
public double getLengthInSeconds() {
|
||||||
return ticksToMilliseconds(length) / 1000.0;
|
return ticksToMilliseconds(length) / 1000.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,29 +11,42 @@ import java.nio.file.Files;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class SongLoader {
|
public class SongLoader {
|
||||||
public static final ArrayList<Song> SONGS = new ArrayList<>();
|
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 final ArrayList<String> SONG_SUGGESTIONS = new ArrayList<>();
|
||||||
public static volatile boolean loadingSongs;
|
public static volatile boolean loadingSongs;
|
||||||
public static volatile boolean showToast;
|
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() {
|
public static void loadSongs() {
|
||||||
if (loadingSongs) return;
|
if (loadingSongs) return;
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
loadingSongs = true;
|
loadingSongs = true;
|
||||||
SONGS.clear();
|
SONGS.clear();
|
||||||
|
FOLDERS.clear();
|
||||||
SONG_SUGGESTIONS.clear();
|
SONG_SUGGESTIONS.clear();
|
||||||
SONG_SUGGESTIONS.add("Songs are loading, please wait");
|
SONG_SUGGESTIONS.add("Songs are loading, please wait");
|
||||||
for (File file : Main.songsFolder.listFiles()) {
|
|
||||||
Song song = null;
|
// Load root folder
|
||||||
try {
|
loadFolder(Main.songsFolder, null);
|
||||||
song = loadSong(file);
|
|
||||||
} catch (Exception exception) {
|
|
||||||
Main.LOGGER.error("Unable to read or parse song {}", file.getName(), exception);
|
|
||||||
}
|
|
||||||
if (song != null) SONGS.add(song);
|
|
||||||
}
|
|
||||||
for (Song song : SONGS) SONG_SUGGESTIONS.add(song.displayName);
|
for (Song song : SONGS) SONG_SUGGESTIONS.add(song.displayName);
|
||||||
Main.config.favorites.removeIf(favorite -> SongLoader.SONGS.stream().map(song -> song.fileName).noneMatch(favorite::equals));
|
Main.config.favorites.removeIf(favorite -> SongLoader.SONGS.stream().map(song -> song.fileName).noneMatch(favorite::equals));
|
||||||
|
|
||||||
@ -43,12 +56,44 @@ public class SongLoader {
|
|||||||
}).start();
|
}).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 {
|
public static Song loadSong(File file) throws IOException {
|
||||||
if (file.isFile()) {
|
if (file.isFile()) {
|
||||||
BinaryReader reader = new BinaryReader(Files.newInputStream(file.toPath()));
|
BinaryReader reader = new BinaryReader(Files.newInputStream(file.toPath()));
|
||||||
Song song = new Song();
|
Song song = new Song();
|
||||||
|
|
||||||
song.fileName = file.getName().replaceAll("[\\n\\r]", "");
|
song.fileName = file.getName().replaceAll("[\\n\\r]", "");
|
||||||
|
song.filePath = file.getPath();
|
||||||
|
|
||||||
song.length = reader.readShort();
|
song.length = reader.readShort();
|
||||||
|
|
||||||
@ -99,7 +144,6 @@ public class SongLoader {
|
|||||||
byte noteId = (byte)(reader.readByte() - 33);
|
byte noteId = (byte)(reader.readByte() - 33);
|
||||||
|
|
||||||
if (newFormat) {
|
if (newFormat) {
|
||||||
// Data that is not needed as it only works with commands
|
|
||||||
reader.readByte(); // Velocity
|
reader.readByte(); // Velocity
|
||||||
reader.readByte(); // Panning
|
reader.readByte(); // Panning
|
||||||
reader.readShort(); // Pitch
|
reader.readShort(); // Pitch
|
||||||
@ -126,5 +170,10 @@ public class SongLoader {
|
|||||||
|
|
||||||
public static void sort() {
|
public static void sort() {
|
||||||
SONGS.sort(Comparator.comparing(song -> song.displayName));
|
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();
|
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() {
|
public synchronized void stopPlaybackThread() {
|
||||||
this.playbackThread = null; // Should stop on its own then
|
this.playbackThread = null; // Should stop on its own then
|
||||||
}
|
}
|
||||||
@ -103,8 +114,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
|||||||
}
|
}
|
||||||
if (running) stop();
|
if (running) stop();
|
||||||
this.song = song;
|
this.song = song;
|
||||||
//Main.LOGGER.info("Song length: " + song.length + " and tempo " + song.tempo);
|
this.loopSong = playMode == PlayMode.SINGLE_LOOP;
|
||||||
//Main.TICK_LISTENERS.add(this);
|
|
||||||
if(this.playbackThread == null) startPlaybackThread();
|
if(this.playbackThread == null) startPlaybackThread();
|
||||||
running = true;
|
running = true;
|
||||||
lastPlaybackTickAt = System.currentTimeMillis();
|
lastPlaybackTickAt = System.currentTimeMillis();
|
||||||
@ -116,6 +126,18 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
|||||||
lastSwingSentAt = -1L;
|
lastSwingSentAt = -1L;
|
||||||
missingInstrumentBlocks.clear();
|
missingInstrumentBlocks.clear();
|
||||||
didSongReachEnd = false;
|
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() {
|
public synchronized void stop() {
|
||||||
@ -215,8 +237,10 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
|||||||
if (index >= song.notes.length) {
|
if (index >= song.notes.length) {
|
||||||
stop();
|
stop();
|
||||||
didSongReachEnd = true;
|
didSongReachEnd = true;
|
||||||
if(loopSong) {
|
if (playMode == PlayMode.SINGLE_LOOP) {
|
||||||
start(song);
|
start(song);
|
||||||
|
} else if (playMode == PlayMode.LIST_LOOP || playMode == PlayMode.RANDOM) {
|
||||||
|
playNextSong();
|
||||||
}
|
}
|
||||||
break;
|
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
|
// 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.
|
// 11/1/2023 Playback now done in separate thread. Not ideal but better especially when FPS are low.
|
||||||
@Override
|
@Override
|
||||||
@ -260,11 +308,11 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
|
|||||||
final Vec3d playerEyePos = player.getEyePos();
|
final Vec3d playerEyePos = player.getEyePos();
|
||||||
|
|
||||||
final int maxOffset; // Rough estimates, of which blocks could be in reach
|
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;
|
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);
|
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));
|
maxOffset = Math.min(7, (int) Math.ceil(player.getBlockInteractionRange() + 1.0 + 1.0));
|
||||||
}else {
|
}else {
|
||||||
throw new NotImplementedException("ExpectedServerVersion Value not implemented: " + Main.config.expectedServerVersion.name());
|
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).
|
// (max distance is BlockInteractRange + 1.0).
|
||||||
private boolean canInteractWith(ClientPlayerEntity player, BlockPos blockPos) {
|
private boolean canInteractWith(ClientPlayerEntity player, BlockPos blockPos) {
|
||||||
final Vec3d eyePos = player.getEyePos();
|
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;
|
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;
|
double blockInteractRange = player.getBlockInteractionRange() + 1.0;
|
||||||
return new Box(blockPos).squaredMagnitude(eyePos) < blockInteractRange * blockInteractRange;
|
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)
|
// Require both checks to succeed (aka use worst distance)
|
||||||
double blockInteractRange = player.getBlockInteractionRange() + 1.0;
|
double blockInteractRange = player.getBlockInteractionRange() + 1.0;
|
||||||
return eyePos.squaredDistanceTo(blockPos.toCenterPos()) <= 6.0 * 6.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 com.mojang.blaze3d.systems.RenderSystem;
|
||||||
import net.minecraft.client.MinecraftClient;
|
import net.minecraft.client.MinecraftClient;
|
||||||
import net.minecraft.client.gui.DrawContext;
|
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.screen.narration.NarrationMessageBuilder;
|
||||||
import net.minecraft.client.gui.widget.EntryListWidget;
|
import net.minecraft.client.gui.widget.EntryListWidget;
|
||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
@ -10,50 +11,44 @@ import net.minecraft.util.Identifier;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import semmiedev.disc_jockey.Main;
|
import semmiedev.disc_jockey.Main;
|
||||||
import semmiedev.disc_jockey.Song;
|
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 FAVORITE_EMOJI = "收藏★";
|
||||||
private static final String NOT_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
|
@Nullable
|
||||||
public int getRowWidth() {
|
public SongEntry getSelectedSongOrNull() {
|
||||||
return width - 40;
|
Entry selected = getSelectedOrNull();
|
||||||
|
return selected instanceof SongEntry ? (SongEntry) selected : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Nullable
|
||||||
protected int getScrollbarX() {
|
public FolderEntry getSelectedFolderOrNull() {
|
||||||
return width - 12;
|
Entry selected = getSelectedOrNull();
|
||||||
|
return selected instanceof FolderEntry ? (FolderEntry) selected : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public static class SongEntry extends Entry {
|
||||||
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> {
|
|
||||||
private static final Identifier ICONS = Identifier.of(Main.MOD_ID, "textures/gui/icons.png");
|
private static final Identifier ICONS = Identifier.of(Main.MOD_ID, "textures/gui/icons.png");
|
||||||
|
|
||||||
public final int index;
|
public final int index;
|
||||||
public final Song song;
|
public final Song song;
|
||||||
|
|
||||||
public boolean selected, favorite;
|
public boolean selected, favorite;
|
||||||
public SongListWidget songListWidget;
|
public SongListWidget songListWidget;
|
||||||
|
|
||||||
private final MinecraftClient client = MinecraftClient.getInstance();
|
private final MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
private int x, y, entryWidth, entryHeight;
|
private int x, y, entryWidth, entryHeight;
|
||||||
|
|
||||||
public SongEntry(Song song, int index) {
|
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.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);
|
String emoji = String.valueOf(favorite ? FAVORITE_EMOJI : NOT_FAVORITE_EMOJI);
|
||||||
|
int emojiWidth = client.textRenderer.getWidth(emoji);
|
||||||
context.drawTextWithShadow(
|
context.drawTextWithShadow(
|
||||||
client.textRenderer,
|
client.textRenderer,
|
||||||
emoji,
|
emoji,
|
||||||
x + 2, y + 2,
|
x + 4, y + 6,
|
||||||
favorite ? 0xFFD700 : 0x808080
|
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
|
@Override
|
||||||
@ -97,12 +103,159 @@ public class SongListWidget extends EntryListWidget<SongListWidget.SongEntry> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isOverFavoriteButton(double mouseX, double mouseY) {
|
private boolean isOverFavoriteButton(double mouseX, double mouseY) {
|
||||||
int textWidth = client.textRenderer.getWidth(favorite ? FAVORITE_EMOJI : NOT_FAVORITE_EMOJI);
|
int iconX = x + 2;
|
||||||
int textHeight = 8;
|
int iconWidth = client.textRenderer.getWidth(favorite ? FAVORITE_EMOJI : NOT_FAVORITE_EMOJI);
|
||||||
return mouseX > x + 2 &&
|
int iconHeight = 8;
|
||||||
mouseX < x + 2 + textWidth &&
|
|
||||||
mouseY > y + 2 &&
|
return mouseX >= iconX &&
|
||||||
mouseY < y + 2 + textHeight;
|
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.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import semmiedev.disc_jockey.SongLoader.SongFolder;
|
||||||
|
import semmiedev.disc_jockey.SongPlayer.PlayMode;
|
||||||
|
|
||||||
public class DiscJockeyScreen extends Screen {
|
public class DiscJockeyScreen extends Screen {
|
||||||
private static final MutableText
|
private static final MutableText
|
||||||
SELECT_SONG = Text.translatable(Main.MOD_ID+".screen.select_song"),
|
SELECT_SONG = Text.translatable(Main.MOD_ID+".screen.select_song"),
|
||||||
@ -36,9 +39,26 @@ public class DiscJockeyScreen extends Screen {
|
|||||||
|
|
||||||
private SongListWidget songListWidget;
|
private SongListWidget songListWidget;
|
||||||
private ButtonWidget playButton, previewButton;
|
private ButtonWidget playButton, previewButton;
|
||||||
private boolean shouldFilter;
|
public boolean shouldFilter;
|
||||||
private String query = "";
|
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() {
|
public DiscJockeyScreen() {
|
||||||
super(Main.NAME);
|
super(Main.NAME);
|
||||||
}
|
}
|
||||||
@ -46,19 +66,56 @@ public class DiscJockeyScreen extends Screen {
|
|||||||
@Override
|
@Override
|
||||||
protected void init() {
|
protected void init() {
|
||||||
shouldFilter = true;
|
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);
|
addDrawableChild(songListWidget);
|
||||||
for (int i = 0; i < SongLoader.SONGS.size(); i++) {
|
for (int i = 0; i < SongLoader.SONGS.size(); i++) {
|
||||||
Song song = SongLoader.SONGS.get(i);
|
Song song = SongLoader.SONGS.get(i);
|
||||||
song.entry.songListWidget = songListWidget;
|
song.entry.songListWidget = songListWidget;
|
||||||
if (song.entry.selected) songListWidget.setSelected(song.entry);
|
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 -> {
|
playButton = ButtonWidget.builder(PLAY, button -> {
|
||||||
if (Main.SONG_PLAYER.running) {
|
if (Main.SONG_PLAYER.running) {
|
||||||
Main.SONG_PLAYER.stop();
|
Main.SONG_PLAYER.stop();
|
||||||
} else {
|
} else {
|
||||||
SongListWidget.SongEntry entry = songListWidget.getSelectedOrNull();
|
SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull();
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
Main.SONG_PLAYER.start(entry.song);
|
Main.SONG_PLAYER.start(entry.song);
|
||||||
client.setScreen(null);
|
client.setScreen(null);
|
||||||
@ -71,7 +128,7 @@ public class DiscJockeyScreen extends Screen {
|
|||||||
if (Main.PREVIEWER.running) {
|
if (Main.PREVIEWER.running) {
|
||||||
Main.PREVIEWER.stop();
|
Main.PREVIEWER.stop();
|
||||||
} else {
|
} else {
|
||||||
SongListWidget.SongEntry entry = songListWidget.getSelectedOrNull();
|
SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull();
|
||||||
if (entry != null) Main.PREVIEWER.start(entry.song);
|
if (entry != null) Main.PREVIEWER.start(entry.song);
|
||||||
}
|
}
|
||||||
}).dimensions(width / 2 - 50, height - 61, 100, 20).build();
|
}).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 -> {
|
addDrawableChild(ButtonWidget.builder(Text.translatable(Main.MOD_ID+".screen.blocks"), button -> {
|
||||||
// TODO: 6/2/2022 Add an auto build mode
|
// TODO: 6/2/2022 Add an auto build mode
|
||||||
if (BlocksOverlay.itemStacks == null) {
|
if (BlocksOverlay.itemStacks == null) {
|
||||||
SongListWidget.SongEntry entry = songListWidget.getSelectedOrNull();
|
SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull();
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
client.setScreen(null);
|
client.setScreen(null);
|
||||||
|
|
||||||
@ -116,7 +173,43 @@ public class DiscJockeyScreen extends Screen {
|
|||||||
}
|
}
|
||||||
}).dimensions(width / 2 + 60, height - 61, 100, 20).build());
|
}).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 -> {
|
searchBar.setChangedListener(query -> {
|
||||||
query = query.toLowerCase().replaceAll("\\s", "");
|
query = query.toLowerCase().replaceAll("\\s", "");
|
||||||
if (this.query.equals(query)) return;
|
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, DROP_HINT, width / 2, 5, 0xFFFFFF);
|
||||||
context.drawCenteredTextWithShadow(textRenderer, SELECT_SONG, width / 2, 20, 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
|
@Override
|
||||||
@ -143,15 +241,59 @@ public class DiscJockeyScreen extends Screen {
|
|||||||
|
|
||||||
if (shouldFilter) {
|
if (shouldFilter) {
|
||||||
shouldFilter = false;
|
shouldFilter = false;
|
||||||
// songListWidget.setScrollAmount(0);
|
|
||||||
songListWidget.children().clear();
|
songListWidget.children().clear();
|
||||||
boolean empty = query.isEmpty();
|
boolean empty = query.isEmpty();
|
||||||
int favoriteIndex = 0;
|
|
||||||
for (Song song : SongLoader.SONGS) {
|
boolean isInSongsOrSubfolder = currentFolder == null ||
|
||||||
if (empty || song.searchableFileName.contains(query) || song.searchableName.contains(query)) {
|
currentFolder.path.startsWith(Main.songsFolder.getPath());
|
||||||
if (song.entry.favorite) {
|
|
||||||
songListWidget.children().add(favoriteIndex++, song.entry);
|
if (currentFolder == null) {
|
||||||
} else {
|
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);
|
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
|
@Override
|
||||||
public void onFilesDropped(List<Path> paths) {
|
public void onFilesDropped(List<Path> paths) {
|
||||||
String string = paths.stream().map(Path::getFileName).map(Path::toString).collect(Collectors.joining(", "));
|
String string = paths.stream().map(Path::getFileName).map(Path::toString).collect(Collectors.joining(", "));
|
||||||
@ -196,6 +372,62 @@ public class DiscJockeyScreen extends Screen {
|
|||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
super.close();
|
super.close();
|
||||||
|
// 保存当前文件夹
|
||||||
|
Main.config.currentFolderPath = currentFolder != null ? currentFolder.path : "";
|
||||||
|
// 保存播放模式
|
||||||
|
Main.config.playMode = currentPlayMode;
|
||||||
|
// 异步保存配置
|
||||||
new Thread(() -> Main.configHolder.save()).start();
|
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_status": "Loop song: %s",
|
||||||
"disc_jockey.loop_enabled": "Enabled looping of current song.",
|
"disc_jockey.loop_enabled": "Enabled looping of current song.",
|
||||||
"disc_jockey.loop_disabled": "Disabled 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",
|
"key.category.disc_jockey": "Disc Jockey",
|
||||||
"disc_jockey.key_bind.open_screen": "Open song selection screen",
|
"disc_jockey.key_bind.open_screen": "Open song selection screen",
|
||||||
"text.autoconfig.disc_jockey.title": "Disc Jockey",
|
"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[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.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": "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_status": "循环播放:%s",
|
||||||
"disc_jockey.loop_enabled": "已启用当前歌曲循环播放",
|
"disc_jockey.loop_enabled": "已启用当前歌曲循环播放",
|
||||||
"disc_jockey.loop_disabled": "已禁用当前歌曲循环播放",
|
"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",
|
"key.category.disc_jockey": "Disc Jockey",
|
||||||
"disc_jockey.key_bind.open_screen": "打开歌曲选择界面",
|
"disc_jockey.key_bind.open_screen": "打开歌曲选择界面",
|
||||||
"text.autoconfig.disc_jockey.title": "Disc Jockey",
|
"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[2]": "版本选择错误可能导致无法触发部分音符盒,影响播放效果",
|
||||||
"text.autoconfig.disc_jockey.option.expectedServerVersion.@Tooltip[3]": "若不确认版本,或需兼容多版本服务器,请选择“全部”",
|
"text.autoconfig.disc_jockey.option.expectedServerVersion.@Tooltip[3]": "若不确认版本,或需兼容多版本服务器,请选择“全部”",
|
||||||
"text.autoconfig.disc_jockey.option.delayPlaybackStartBySecs": "播放延迟(秒)",
|
"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