diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..185f286
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..50c2ba1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+bin/
+Test Data/
\ No newline at end of file
diff --git a/.project b/.project
new file mode 100644
index 0000000..6976b44
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+
+
+ PlaylistEditor
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..b6593d2
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,12 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "java",
+ "name": "Debug (Launch)-PlaylistWindow",
+ "request": "launch",
+ "mainClass": "com.pe.PlaylistWindow",
+ "projectName": "PlaylistEditor"
+ }
+ ]
+}
diff --git a/External JARs/MP3agic.jar b/External JARs/MP3agic.jar
new file mode 100644
index 0000000..2c91b89
Binary files /dev/null and b/External JARs/MP3agic.jar differ
diff --git a/External JARs/jlayer-1.0.1.jar b/External JARs/jlayer-1.0.1.jar
new file mode 100644
index 0000000..b4870bc
Binary files /dev/null and b/External JARs/jlayer-1.0.1.jar differ
diff --git a/defaults.conf b/defaults.conf
new file mode 100644
index 0000000..75e199f
--- /dev/null
+++ b/defaults.conf
@@ -0,0 +1 @@
+C:\Workspaces\java-playlist-editor\Test DataH:\Documents\Other\Eclipse Workspace\PlaylistEditor\Test Data
diff --git a/src/com/pe/Playlist.java b/src/com/pe/Playlist.java
new file mode 100644
index 0000000..6878dda
--- /dev/null
+++ b/src/com/pe/Playlist.java
@@ -0,0 +1,393 @@
+package com.pe;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.io.PrintWriter;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * Represents a playlist.
+ *
+ * @author olly.rowe
+ */
+public class Playlist
+{
+ // The playlist attributes
+ private String name;
+ private String author;
+ private ArrayList songs;
+
+ // The file to where the playlist is stored
+ private File file;
+
+ // Parser constants
+ public static final String WPL = "wpl";
+ public static final String M3U = "m3u";
+ private static final String WPL_FILE_PREFIX = "";
+
+ /**
+ * Class Constructor for new playlist.
+ */
+ public Playlist()
+ {
+ this.name = new String();
+ this.author = new String();
+ this.songs = new ArrayList();
+ }
+
+ /**
+ * Alternative Class Constructor for existing files.
+ *
+ * @param File - The file containing the playlist data.
+ */
+ public Playlist(File file) throws FileNotFoundException
+ {
+ // Set the file
+ this.file = file;
+
+ // Initialise the songs array
+ this.songs = new ArrayList();
+
+ // Check that the correct file type as been passed
+ if (file.getName().endsWith(".wpl"))
+ {
+ // Try to read the data from the file
+ try
+ {
+ DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
+ Document doc = docBuilder.parse(file);
+ Node titleNode = doc.getElementsByTagName("title").item(0);
+
+ this.name = titleNode.getTextContent();
+
+ Node authorNode = doc.getElementsByTagName("author").item(0);
+
+ this.author = authorNode.getTextContent();
+
+ NodeList mediaNodes = doc.getElementsByTagName("media");
+
+ for (int i = 0; i < mediaNodes.getLength(); i++)
+ {
+ Song song = new Song(mediaNodes.item(i).getAttributes().getNamedItem("src").getTextContent());
+
+ this.songs.add(song);
+ }
+ }
+ catch (FileNotFoundException e)
+ {
+ throw new FileNotFoundException();
+ }
+ catch (ParserConfigurationException | SAXException | IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ else
+ {
+ System.err.println("Playlist initialisation error. Unsupportted file format detected.");
+ }
+ }
+
+ /**
+ * Calls the corresponding parser and writes the output to the file
+ */
+ public void save(String fileFormat)
+ {
+ // Initialise output string
+ String output = "";
+
+ // Choose the correct parser based on the passed file format value
+ switch(fileFormat)
+ {
+ case WPL:
+ {
+ output = this.parseToWPL();
+ break;
+ }
+ case M3U:
+ {
+ break;
+ }
+ default:
+ {
+ System.err.println("Failed attempt to save playist. Unsupported file format: " + fileFormat);
+ return;
+ }
+ }
+
+ // Write the output to the file
+ try
+ {
+ PrintWriter pw = new PrintWriter(this.getFile().getAbsolutePath());
+ pw.println(output);
+ pw.close();
+ }
+ catch (FileNotFoundException e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Parser for WPL file format.
+ */
+ public String parseToWPL()
+ {
+ // Initialise the output string
+ String output = WPL_FILE_PREFIX;
+
+ // Attempt to parse playlist
+ try
+ {
+ // Adapted from: https://examples.javacodegeeks.com/core-java/xml/parsers/documentbuilderfactory/create-xml-file-in-java-using-dom-parser-example/
+ DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder documentBuilder = documentFactory.newDocumentBuilder();
+ Document document = documentBuilder.newDocument();
+
+ Element smil = document.createElement("smil");
+ document.appendChild(smil);
+
+ Element head = document.createElement("head");
+
+ smil.appendChild(head);
+
+ Element title = document.createElement("title");
+
+ title.appendChild(document.createTextNode(this.getName()));
+
+ head.appendChild(title);
+
+ Element author = document.createElement("author");
+
+ author.appendChild(document.createTextNode(this.getAuthor()));
+
+ head.appendChild(author);
+
+ Element body = document.createElement("body");
+
+ smil.appendChild(body);
+
+ Element seq = document.createElement("seq");
+
+ body.appendChild(seq);
+
+ for (Song song : this.songs)
+ {
+ // Create a new media tag for the song
+ Element media = document.createElement("media");
+
+ // Set the src attribute to the absolute path of its corresponding file location
+ media.setAttribute("src", song.getFile().getAbsolutePath());
+ // Set the albumTitle attribute
+ media.setAttribute("albumTitle", song.getAlbum());
+ // Set the albumArtist attribute
+ media.setAttribute("albumArtist", song.getArtist());
+ // Set the trackTitle attribute
+ media.setAttribute("trackTitle", song.getTitle());
+ // Set the trackArtist attribute
+ media.setAttribute("trackArtist", song.getArtist());
+ // Set the duration attribute
+ media.setAttribute("duration", Long.toString(song.getLength()));
+
+ // Add the media tag to the seq tag
+ seq.appendChild(media);
+ }
+
+ // Create new transformation objects
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ Transformer transformer = transformerFactory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ DOMSource domSource = new DOMSource(document);
+ // Create a new StringWriter object to output the result to
+ StringWriter outWriter = new StringWriter();
+ // The stream result, outputting to the String Writer
+ StreamResult streamResult = new StreamResult(outWriter);
+ // Transform the DOM Object to an XML File
+ transformer.transform(domSource, streamResult);
+ StringBuffer sb = outWriter.getBuffer();
+ // Output result to the output string
+ output += sb.toString();
+ }
+ catch (ParserConfigurationException | TransformerException e)
+ {
+ e.printStackTrace();
+ }
+ return output;
+ }
+
+ /**
+ * Identifies whether this playlist is currently saved.
+ *
+ * Also returns true in the special case where the playlist details are completely blank.
+ *
+ * @return - Whether the playlist is saved.
+ */
+ public boolean isSaved()
+ {
+ // Check whether the detials are blank
+ if (this.file == null && this.author.equals("") && this.name.equals("") && this.songs.size() == 0)
+ {
+ // If all details are blank then there is no need to save, therefore return true in this case
+ return true;
+ }
+
+ // Check if this playlist has a file associated with it yet
+ if (this.file == null)
+ {
+ // If there is not an associated file then it has not yet been saved
+ return false;
+ }
+ // Parse the current state of this playlist
+ String parserOutput = this.parseToWPL();
+
+ // Read the corresponding file
+ String fileOutput = new String();
+ try
+ {
+ fileOutput = Files.readAllLines(this.file.toPath()).get(0);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+
+ // Compare the file's contents with the parser output and then return whether they match or not
+ if (parserOutput.equals(fileOutput))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ public String getName()
+ {
+ return this.name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getAuthor()
+ {
+ return this.author;
+ }
+
+ public void setAuthor(String author)
+ {
+ this.author = author;
+ }
+
+ public ArrayList getSongs()
+ {
+ return this.songs;
+ }
+
+ public Song getSong(int index)
+ {
+ // Check that the index is valid
+ if (index <= (this.songs.size() - 1) && index >= 0)
+ {
+ return this.songs.get(index);
+ }
+ return null;
+ }
+
+ public int getIndexOf(Song song)
+ {
+ return this.songs.indexOf(song);
+ }
+
+ public Song getLastSong()
+ {
+ // Check that the songs array isn't empty
+ if (this.songs.size() != 0)
+ {
+ return this.songs.get(this.songs.size() - 1);
+ }
+ return null;
+ }
+
+ public void addSong(Song song)
+ {
+ this.songs.add(song);
+ }
+
+ public void removeSong(int songIndex)
+ {
+ this.songs.remove(songIndex);
+ }
+
+ /**
+ * Swaps the order of two songs on the playlist.
+ *
+ * @param index1 - The index of the first song to be swapped.
+ * @param index2 - The index of the seconds song to be swapped.
+ */
+ public void swapSongs(int index1, int index2)
+ {
+ Collections.swap(this.songs, index1, index2);
+ }
+
+ public File getFile()
+ {
+ return this.file;
+ }
+
+ public void setFile(File file)
+ {
+ this.file = file;
+ }
+
+ /**
+ * Returns the size of the songs array.
+ */
+ public int getSize()
+ {
+ return this.songs.size();
+ }
+
+ /**
+ * Calculates the total play time of all of the songs on this playlist.
+ *
+ * @return - A String value representing the total play time in hours, minutes and seconds.
+ */
+ public String getPlayTime()
+ {
+ long playTime = 0;
+
+ for (Song song : this.songs)
+ {
+ playTime += song.getLength();
+ }
+
+ int hours = (int) playTime / 3600;
+ int remainder = (int) playTime - hours * 3600;
+ int mins = remainder / 60;
+ remainder = remainder - mins * 60;
+ int secs = remainder;
+
+ return (((hours == 0) ? "" : hours + "h ") + ((hours == 0 && mins == 0) ? "" : mins + "m ") + secs + "s");
+ }
+}
diff --git a/src/com/pe/PlaylistWindow.java b/src/com/pe/PlaylistWindow.java
new file mode 100644
index 0000000..aeb6b77
--- /dev/null
+++ b/src/com/pe/PlaylistWindow.java
@@ -0,0 +1,895 @@
+package com.pe;
+
+import com.pe.Song;
+import com.pe.Playlist;
+import com.pe.UserDefaults;
+import com.pe.utils.FileTree;
+import com.pe.utils.FileNode;
+import com.pe.audio.MusicPlayer;
+import java.awt.EventQueue;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.tree.TreePath;
+import javax.swing.JTable;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
+import java.awt.Color;
+import java.awt.Dimension;
+import javax.swing.JButton;
+import javax.swing.JFileChooser;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.awt.event.ActionEvent;
+import javax.swing.JTextField;
+import javax.swing.JTextPane;
+import javax.swing.JLabel;
+import javax.swing.JScrollPane;
+import javax.swing.ScrollPaneConstants;
+import java.awt.Toolkit;
+import javax.swing.JMenuBar;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JSeparator;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.Image;
+import javax.swing.ImageIcon;
+
+/**
+ * The main GUI for the playlist editor.
+ *
+ * @author olly.rowe
+ */
+@SuppressWarnings("serial")
+public class PlaylistWindow extends JFrame implements ActionListener {
+ // The current version number
+ private final String VERSION = "1.0.0";
+
+ // The user defaults object
+ private UserDefaults userDefaults;
+
+ // The playlist being edited
+ private Playlist playlist;
+
+ // The song table and table model
+ private JTable songTable;
+ private DefaultTableModel songTableModel;
+
+ // The playlist details text fields
+ private JTextField playlistNameTextField;
+ private JTextField playlistAuthorTextField;
+ private JTextField playlistSizeTextField;
+ private JTextField playlistPlayTimeTextField;
+
+ // The menu bar components
+ private JMenuItem mntmNew;
+ private JMenuItem mntmOpen;
+ private JMenuItem mntmSave;
+ private JMenuItem mntmExit;
+ private JMenuItem mntmSplitTrack;
+ private JMenuItem mntmReleaseNotes;
+
+ // The button components
+ private JButton changeDirectoryButton;
+ private JButton addSongButton;
+ private JButton removeSongButton;
+ private JButton saveButton;
+ private JButton exitButton;
+ private JButton upArrowButton;
+ private JButton downArrowButton;
+
+ // The file explorer tree
+ private FileTree fileTree;
+
+ // The music player component
+ private MusicPlayer musicPlayer;
+ private JPanel albumArtPanel;
+
+ /**
+ * Launch the application.
+ */
+ public static void main(String[] args) {
+ EventQueue.invokeLater(new Runnable() {
+ public void run() {
+ try {
+ PlaylistWindow frame = new PlaylistWindow();
+ frame.setVisible(true);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ /**
+ * Class Constructor. Generates the GUI.
+ */
+ public PlaylistWindow() {
+ // Create the playlist object
+ this.playlist = new Playlist();
+
+ // Initialise the user defaults
+ this.userDefaults = new UserDefaults();
+
+ // Set the window title
+ setTitle("Playlist Editor " + VERSION);
+ // Set the window icon
+ setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/images/icon.png")));
+ // Prevent the window from being resized
+ setResizable(false);
+
+ // Set the GUI appearance to match that of the users operating system
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ UIManager.put("Slider.focus", UIManager.get("Slider.background"));
+ } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
+ | UnsupportedLookAndFeelException e) {
+ System.err.println("An error occured setting the UIManager look and feel.");
+ }
+
+ // Set the window dimensions
+ setBounds(100, 100, 800, 540);
+
+ // Set default closing operation to nothing
+ setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+ // Add custom closing operation
+ addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent evt) {
+ exit();
+ }
+ });
+
+ // Create the menu bar
+ JMenuBar menuBar = new JMenuBar();
+ setJMenuBar(menuBar);
+ // Add menu components to the menu bar
+ JMenu mnFile = new JMenu("File");
+ menuBar.add(mnFile);
+ mntmNew = new JMenuItem("Create New Playlist...");
+ mntmNew.addActionListener(this);
+ mnFile.add(mntmNew);
+ mntmOpen = new JMenuItem("Open Existing Playlist...");
+ mntmOpen.addActionListener(this);
+ mnFile.add(mntmOpen);
+ mntmSave = new JMenuItem("Save");
+ mntmSave.addActionListener(this);
+ mnFile.add(mntmSave);
+ JSeparator separator = new JSeparator();
+ mnFile.add(separator);
+ mntmExit = new JMenuItem("Exit");
+ mntmExit.addActionListener(this);
+ mnFile.add(mntmExit);
+ JMenu mnTools = new JMenu("Tools");
+ menuBar.add(mnTools);
+ mntmSplitTrack = new JMenuItem("Split Track");
+ mntmSplitTrack.addActionListener(this);
+ mnTools.add(mntmSplitTrack);
+ JMenu mnAbout = new JMenu("About");
+ menuBar.add(mnAbout);
+ mntmReleaseNotes = new JMenuItem("Release Notes");
+ mntmReleaseNotes.addActionListener(this);
+ mnAbout.add(mntmReleaseNotes);
+
+ // Create the main content pane
+ JPanel contentPane = new JPanel();
+ contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
+ setContentPane(contentPane);
+ contentPane.setLayout(null);
+
+ // Create the track list section
+ JPanel trackListPanel = new JPanel();
+ trackListPanel.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Track List",
+ TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0)));
+ trackListPanel.setBounds(338, 87, 446, 262);
+ contentPane.add(trackListPanel);
+ trackListPanel.setLayout(null);
+
+ // Create the song table and table model
+ songTable = new JTable();
+ songTableModel = new DefaultTableModel(new Object[][] {}, new String[] { "Track No.", "Artist", "Title" }) {
+ // Disable cell-editing
+ @Override
+ public boolean isCellEditable(int row, int column) {
+ return false;
+ }
+ };
+ // Set the table model
+ songTable.setModel(songTableModel);
+ // Set table attributes
+ songTable.getColumnModel().getColumn(0).setPreferredWidth(56);
+ songTable.getColumnModel().getColumn(0).setMaxWidth(56);
+ songTable.setColumnSelectionAllowed(false);
+ songTable.setFillsViewportHeight(true);
+ songTable.setRowSelectionAllowed(true);
+
+ // Create a new scroll pane and assign the songs table to it
+ JScrollPane songTableScrollPane = new JScrollPane(songTable);
+ songTableScrollPane.setBounds(10, 21, 426, 196);
+ songTableScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+ songTableScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
+ trackListPanel.add(songTableScrollPane);
+
+ // Create remove song button
+ removeSongButton = new JButton("Remove");
+ removeSongButton.setBounds(347, 228, 89, 23);
+ removeSongButton.addActionListener(this);
+ trackListPanel.add(removeSongButton);
+
+ Image upArrowIcon = (new ImageIcon(getClass().getResource("/images/arrow up.png"))).getImage()
+ .getScaledInstance(10, 10, Image.SCALE_SMOOTH);
+ upArrowButton = new JButton(new ImageIcon(upArrowIcon));
+ upArrowButton.addActionListener(this);
+ upArrowButton.setBounds(10, 228, 23, 23);
+ trackListPanel.add(upArrowButton);
+
+ Image downArrowIcon = (new ImageIcon(getClass().getResource("/images/arrow down.png"))).getImage()
+ .getScaledInstance(10, 10, Image.SCALE_SMOOTH);
+ downArrowButton = new JButton(new ImageIcon(downArrowIcon));
+ downArrowButton.addActionListener(this);
+ downArrowButton.setBounds(37, 228, 23, 23);
+ trackListPanel.add(downArrowButton);
+
+ // Create browse for music panel
+ JPanel browseForMusicPanel = new JPanel();
+ browseForMusicPanel.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Browse for Music",
+ TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0)));
+ browseForMusicPanel.setBounds(10, 11, 318, 469);
+ contentPane.add(browseForMusicPanel);
+ browseForMusicPanel.setLayout(null);
+
+ // Create add song to playlist button
+ addSongButton = new JButton("Add");
+ addSongButton.setBounds(190, 435, 118, 23);
+ addSongButton.addActionListener(this);
+ browseForMusicPanel.add(addSongButton);
+
+ // Change the default leaf icon to music note. As the tree will filter mp3
+ // files, only mp3 files will display this icon
+ UIManager.put("Tree.leafIcon", new ImageIcon((new ImageIcon(getClass().getResource("/images/music note.png")))
+ .getImage().getScaledInstance(14, 14, Image.SCALE_SMOOTH)));
+
+ // Create the file explorer tree using the default directory if one exists,
+ // otherwise use the user's C: drive. Filter it to show only mp3 files
+ fileTree = new FileTree(
+ (this.userDefaults.getDefaultBrowserDir() == null) ? "C:\\" : this.userDefaults.getDefaultBrowserDir(),
+ Arrays.asList(".mp3"));
+
+ // Add mouse listener to detect double clicks
+ fileTree.addMouseListener(new MouseAdapter() {
+ public void mousePressed(MouseEvent e) {
+ if (fileTree.getRowForLocation(e.getX(), e.getY()) != -1) {
+ FileNode selectedNode = (FileNode) fileTree.getPathForLocation(e.getX(), e.getY())
+ .getLastPathComponent();
+
+ // If a double-click is detected, add the song to the current playlist
+ if (e.getClickCount() == 2) {
+ addSong(selectedNode);
+ }
+ }
+ }
+ });
+
+ // Create a scroll pane and add the file tree to it
+ JScrollPane fileTreeScrollPane = new JScrollPane(fileTree);
+ fileTree.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));
+ fileTreeScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
+ fileTreeScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
+ fileTreeScrollPane.setBounds(10, 22, 298, 402);
+ browseForMusicPanel.add(fileTreeScrollPane);
+
+ // Create change directory button which changes contents of file tree
+ changeDirectoryButton = new JButton("...");
+ changeDirectoryButton.setBounds(10, 435, 42, 23);
+ browseForMusicPanel.add(changeDirectoryButton);
+ changeDirectoryButton.addActionListener(this);
+
+ // Create new playlist details panel
+ JPanel playlistDetailsPanel = new JPanel();
+ playlistDetailsPanel.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Details",
+ TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0)));
+ playlistDetailsPanel.setBounds(338, 11, 446, 65);
+ contentPane.add(playlistDetailsPanel);
+ playlistDetailsPanel.setLayout(null);
+
+ // Create playlist name label and text field
+ JLabel lblName = new JLabel("Name");
+ lblName.setBounds(10, 18, 46, 14);
+ playlistDetailsPanel.add(lblName);
+ playlistNameTextField = new JTextField();
+ playlistNameTextField.setBounds(54, 14, 241, 20);
+ playlistDetailsPanel.add(playlistNameTextField);
+ playlistNameTextField.setColumns(10);
+ addChangeListener(playlistNameTextField);
+
+ // Create playlist author label and text field
+ JLabel lblAuthor = new JLabel("Author");
+ lblAuthor.setBounds(10, 42, 46, 14);
+ playlistDetailsPanel.add(lblAuthor);
+ playlistAuthorTextField = new JTextField();
+ playlistAuthorTextField.setBounds(54, 38, 241, 20);
+ playlistDetailsPanel.add(playlistAuthorTextField);
+ playlistAuthorTextField.setColumns(10);
+ addChangeListener(playlistAuthorTextField);
+
+ // Create playlist size label and text field
+ JLabel lblLength = new JLabel("Size");
+ lblLength.setBounds(305, 16, 46, 14);
+ playlistDetailsPanel.add(lblLength);
+ playlistSizeTextField = new JTextField();
+ playlistSizeTextField.setText("0");
+ playlistSizeTextField.setEditable(false);
+ playlistSizeTextField.setBounds(364, 12, 72, 20);
+ playlistDetailsPanel.add(playlistSizeTextField);
+ playlistSizeTextField.setColumns(10);
+
+ // Create playlist play time label and text field
+ JLabel lblPlayTime = new JLabel("Play Time");
+ lblPlayTime.setBounds(305, 40, 46, 14);
+ playlistDetailsPanel.add(lblPlayTime);
+ playlistPlayTimeTextField = new JTextField();
+ playlistPlayTimeTextField.setText("0s");
+ playlistPlayTimeTextField.setEditable(false);
+ playlistPlayTimeTextField.setColumns(10);
+ playlistPlayTimeTextField.setBounds(364, 36, 72, 20);
+ playlistDetailsPanel.add(playlistPlayTimeTextField);
+
+ // Create the save button
+ saveButton = new JButton("Save");
+ saveButton.setBounds(596, 457, 89, 23);
+ saveButton.addActionListener(this);
+ contentPane.add(saveButton);
+
+ // Create the exit button
+ exitButton = new JButton("Exit");
+ exitButton.setBounds(695, 457, 89, 23);
+ exitButton.addActionListener(this);
+ contentPane.add(exitButton);
+
+ JPanel panel = new JPanel();
+ panel.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Music Playback",
+ TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0)));
+ panel.setBounds(338, 360, 356, 86);
+ contentPane.add(panel);
+ panel.setLayout(null);
+
+ albumArtPanel = new JPanel();
+ albumArtPanel.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "", TitledBorder.LEADING,
+ TitledBorder.TOP, null, new Color(0, 0, 0)));
+ albumArtPanel.setBounds(704, 366, 78, 78);
+ contentPane.add(albumArtPanel);
+ albumArtPanel.setLayout(null);
+
+ JLabel lblAlbumArt = new JLabel();
+ lblAlbumArt.setBounds(4, 4, 70, 70);
+ albumArtPanel.add(lblAlbumArt);
+
+ // Create the music player component
+ musicPlayer = new MusicPlayer(this.playlist, lblAlbumArt);
+ musicPlayer.setBounds(13, 16, 330, 64);
+ panel.add(musicPlayer);
+ }
+
+ /**
+ * Button Action listener handler.
+ */
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ Object source = e.getSource();
+
+ if (source == changeDirectoryButton) {
+ browseDirectory();
+ } else if (source == addSongButton) {
+ addSelectedSongs();
+ } else if (source == removeSongButton) {
+ removeSelectedSongs();
+ } else if ((source == saveButton) || (source == mntmSave)) {
+ save();
+ } else if ((source == exitButton) || (source == mntmExit)) {
+ exit();
+ } else if (source == mntmOpen) {
+ open();
+ } else if ((source == upArrowButton) || (source == downArrowButton)) {
+ reorderSongs(source);
+ } else if (source == mntmNew) {
+ createNewPlaylist(null);
+ } else if (source == mntmSplitTrack) {
+ splitTrack();
+ } else if (source == mntmReleaseNotes) {
+ displayReleaseNotes();
+ }
+ }
+
+ /**
+ * Allows the user to browse for a directory to populate the tree view.
+ */
+ public void browseDirectory() {
+ // The file chooser
+ JFileChooser fileChooser = new JFileChooser();
+ // Allow the user to only be able to select directories
+ fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+
+ // Show the dialog and if a directory is selected, update the file tree
+ if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+ // Update the directory within the file tree
+ fileTree.setDirectory(fileChooser.getSelectedFile().getAbsolutePath());
+ // Update the user defaults with the new directory
+ this.userDefaults.setDefaultBrowserDir(fileChooser.getSelectedFile().getAbsolutePath());
+ }
+ return;
+ }
+
+ /**
+ * Allows the user to browse for a playlist file to be opened.
+ */
+ public void open() {
+ // The file chooser. Setting the current directory from the user defaults
+ JFileChooser fileChooser = new JFileChooser(new File(
+ ((this.userDefaults.getDefaultOpenPlaylistDir() != null) ? this.userDefaults.getDefaultOpenPlaylistDir()
+ : "")));
+ // Create new file filter for the playlist file formats
+ FileNameExtensionFilter filter = new FileNameExtensionFilter(null, "wpl");
+ // Set the filter
+ fileChooser.setFileFilter(filter);
+ // Allow the user to only be able to select directories
+ fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+
+ // Show the dialog and if a directory is selected, update the file tree
+ if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+ // Check that the file is of the correct format
+ if (fileChooser.getSelectedFile().getName().endsWith(".wpl")) {
+ this.createNewPlaylist(fileChooser.getSelectedFile());
+ // Set the default for the location of the directory that this file is in
+ this.userDefaults.setDefaultOpenPlaylistDir(fileChooser.getSelectedFile().getParent());
+ }
+ }
+ }
+
+ /**
+ * Clears the GUI and creates a new playlist instance.
+ */
+ public void createNewPlaylist(File file) {
+ // If the current playlist isn't yet saved then ask the user if they would like
+ // to save it
+ if (!this.playlist.isSaved()) {
+ // Ask the user if they want to save the current playlist
+ int saveDialog = JOptionPane.showConfirmDialog(this, "Would you like to save the current playlist?");
+
+ // If the user confirmed the dialog
+ if (saveDialog == JOptionPane.YES_OPTION) {
+ // Save the current playlist
+ boolean saveSuccessful = this.save();
+ // If the save was unsuccessful then exit this method
+ if (!saveSuccessful) {
+ return;
+ }
+ }
+ // If the user cancelled the dialog, then do not create new playlist
+ else if (saveDialog == JOptionPane.CANCEL_OPTION || saveDialog == JOptionPane.CLOSED_OPTION) {
+ return;
+ }
+ }
+
+ // If a file has been specified then pass it into the playlist constructor when
+ // creating the new playlist
+ if (file != null) {
+ try {
+ this.playlist = new Playlist(file);
+ } catch (FileNotFoundException e) {
+ this.raiseFileNotFoundWarning(file.getAbsolutePath());
+ }
+ } else {
+ this.playlist = new Playlist();
+ }
+
+ // Refresh the GUI
+ this.refreshPlaylistDetails();
+
+ // Update the music player with the new playlist
+ this.musicPlayer.updatePlaylist(this.playlist);
+ }
+
+ /**
+ * Updates all components that hold information about the playlist with current
+ * information.
+ */
+ public void refreshPlaylistDetails() {
+ // Update the playlist details section
+ this.playlistNameTextField.setText(this.playlist.getName());
+ this.playlistAuthorTextField.setText(this.playlist.getAuthor());
+ this.playlistSizeTextField.setText(String.valueOf(this.playlist.getSize()));
+ this.playlistPlayTimeTextField.setText(this.playlist.getPlayTime());
+
+ // Remove any songs from the songs table
+ int numOfRows = this.songTable.getModel().getRowCount();
+ // Iterate through songs currently in table
+ for (int i = 0; i < numOfRows; i++) {
+ // Remove the song from the table
+ ((DefaultTableModel) this.songTable.getModel()).removeRow(0);
+ }
+
+ // Update the songs table
+ for (Song song : this.playlist.getSongs()) {
+ songTableModel.addRow(
+ new Object[] { (this.playlist.getSongs().indexOf(song) + 1), song.getArtist(), song.getTitle() });
+ }
+ }
+
+ public void addSong(FileNode nodeOfFile) {
+ File file = nodeOfFile.getFile();
+
+ if (file.getName().endsWith(".mp3")) {
+ try {
+ // Create song object
+ Song song = new Song(file.getAbsolutePath());
+ // Add the song to the playlist
+ this.playlist.addSong(song);
+
+ // If the playlist was empty before adding the song then set this song as the
+ // current one
+ if (this.playlist.getSize() == 1) {
+ this.musicPlayer.setCurrentSong(song);
+ // Refresh the GUI components
+ this.musicPlayer.updateGUI();
+ }
+
+ // Add the song to the table
+ songTableModel.addRow(new Object[] { this.playlist.getSize(), song.getArtist(), song.getTitle() });
+
+ // Refresh the playlist details
+ refreshPlaylistDetails();
+ } catch (FileNotFoundException e) {
+ this.raiseFileNotFoundWarning(file.getAbsolutePath());
+ }
+ }
+ }
+
+ /**
+ * Adds the songs that are currently selected in the file tree to the tracks
+ * table.
+ */
+ public void addSelectedSongs() {
+ // Check that something has been selected in the file tree
+ if (fileTree.getSelectionPath() != null) {
+ for (TreePath tp : fileTree.getSelectionPaths()) {
+ addSong((FileNode) tp.getLastPathComponent());
+ }
+ }
+ }
+
+ /**
+ * Removes the songs that are currently selected in the songs table from the
+ * playlist.
+ */
+ public void removeSelectedSongs() {
+ // Get the number of selected rows
+ int numOfSelectedRows = this.songTable.getSelectedRowCount();
+
+ // Iterate through the selected rows
+ for (int i = 0; i < numOfSelectedRows; i++) {
+ int index = this.songTable.getSelectedRows()[0];
+ // Remove the song from the playlist
+ this.playlist.removeSong(index);
+ // Remove the song from the table
+ ((DefaultTableModel) this.songTable.getModel()).removeRow(index);
+ }
+ // Refresh the playlist details
+ refreshPlaylistDetails();
+ }
+
+ /**
+ * Updates the track numbers in the songs table.
+ */
+ public void updateTrackNumbers() {
+ // Update the track numbers in the songs table
+ for (int i = 0; i < this.songTable.getModel().getRowCount(); i++) {
+ this.songTable.getModel().setValueAt((i + 1), i, 0);
+ }
+ }
+
+ /**
+ * Reorders the songs within the songs table upon clicking up or down arrow
+ * button.
+ *
+ * @param source - The source object that trigger the action event.
+ */
+ public void reorderSongs(Object source) {
+ // Ensure that only one row has been selected
+ if (this.songTable.getSelectedRowCount() == 1) {
+ // Get the index of the selected row
+ int indexOfSelectedRow = this.songTable.getSelectedRow();
+
+ // Get the table model
+ DefaultTableModel tableModel = ((DefaultTableModel) this.songTable.getModel());
+
+ // If the up arrow button was clicked
+ if (source == this.upArrowButton) {
+ // Check that the selected row isn't already at the start
+ if (indexOfSelectedRow != 0) {
+ // Unselected the selected row
+ this.songTable.clearSelection();
+ // Move the row up in the table
+ tableModel.moveRow(indexOfSelectedRow, indexOfSelectedRow, (indexOfSelectedRow - 1));
+ // Re-select the moved row
+ this.songTable.setRowSelectionInterval((indexOfSelectedRow - 1), (indexOfSelectedRow - 1));
+ // Swap the two rows that have changed position in the playlist
+ this.playlist.swapSongs(indexOfSelectedRow, (indexOfSelectedRow - 1));
+ }
+ }
+ // Otherwise, if the down arrow button was clicked
+ else if (source == this.downArrowButton) {
+ // Check that the selected row isn't already at the end
+ if (indexOfSelectedRow != (this.songTable.getRowCount() - 1)) {
+ // Unselected the selected row
+ this.songTable.clearSelection();
+ // Move the row down in the table
+ tableModel.moveRow(indexOfSelectedRow, indexOfSelectedRow, (indexOfSelectedRow + 1));
+ // Re-select the moved row
+ this.songTable.setRowSelectionInterval((indexOfSelectedRow + 1), (indexOfSelectedRow + 1));
+ // Swap the two rows that have changed position in the playlist
+ this.playlist.swapSongs(indexOfSelectedRow, (indexOfSelectedRow + 1));
+ }
+ }
+ //
+ this.songTable.scrollRectToVisible(this.songTable.getCellRect(this.songTable.getSelectedRow(), 0, true));
+ // Update the track numbers in the songs table
+ updateTrackNumbers();
+ }
+ }
+
+ /**
+ * Saves the playlist currently being edited.
+ *
+ * @return Boolean value indicating whether the save was successful or not.
+ */
+ public boolean save() {
+ // Check if the playlist has an associated file, if not prompt the user to
+ // select one
+ if (playlist.getFile() == null) {
+ // Display the file system for the user to pick a location to save the playlist
+ JFileChooser fileChooser = new JFileChooser();
+ // Set the file name within the file chooser
+ fileChooser.setSelectedFile(new File(playlist.getName() + ".wpl"));
+ // If the user has selected a file
+ if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
+ // Fetch the selected file
+ File chosenFile = fileChooser.getSelectedFile();
+ // Check that the file is of the right type
+ if (chosenFile.getAbsolutePath().endsWith(Playlist.WPL)) {
+ // If the file already exists, then as the user to confirm overwritting the
+ // playlist
+ if (chosenFile.exists()) {
+ int confirmReplaceFileResult = JOptionPane.showConfirmDialog(this,
+ chosenFile.getName() + " already exists. Do you want to replace it?", "Confirm Save As",
+ JOptionPane.YES_NO_OPTION);
+ // If the user didn't says yes to the popup then exit the method
+ if (!(confirmReplaceFileResult == JOptionPane.YES_OPTION)) {
+ return false;
+ }
+ }
+ // Set the playlist's file
+ this.playlist.setFile(fileChooser.getSelectedFile().getAbsoluteFile());
+ }
+ } else {
+ return false;
+ }
+ }
+ // Save the playlist
+ this.playlist.save(Playlist.WPL);
+ // Display popup
+ JOptionPane.showMessageDialog(this, "Your playlist has been saved.");
+ return true;
+ }
+
+ /**
+ * Splits the current track within the music player and allows the user to
+ * export it to a new file.
+ */
+ public void splitTrack() {
+ // If their isn't currently a song loaded into the music player then raise an
+ // error message to the user and exit method
+ if (this.musicPlayer.getCurrentSong() == null) {
+ JOptionPane.showMessageDialog(this, "Could not split as there is no current song. Play a song to split it.",
+ "Split Track Failed", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ // Ask the user whether they want to keep the first or second part of the
+ // current track
+ int splitChoice = JOptionPane.showOptionDialog(this,
+ "Would you like to export the first or second part of the split track?", "Split Track Tool",
+ JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, null,
+ new Object[] { "First Part", "Second Part" }, null);
+
+ // If the user closed the dialog then exit method
+ if (splitChoice == JOptionPane.CLOSED_OPTION) {
+ return;
+ }
+
+ // Ask the user to select a file to write the new split track to
+ JFileChooser fileChooser = new JFileChooser(new File(
+ ((this.userDefaults.getDefaultOpenPlaylistDir() != null) ? this.userDefaults.getDefaultOpenPlaylistDir()
+ : "")));
+ // Create new file filter for mp3 file format
+ FileNameExtensionFilter filter = new FileNameExtensionFilter(null, "mp3");
+ // Set the filter
+ fileChooser.setFileFilter(filter);
+ // Allow the user to only be able to select files
+ fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+
+ // Show the dialog and if a directory is selected, update the file tree
+ if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+ // The file chosen by the user
+ File selectedFile = fileChooser.getSelectedFile();
+
+ // Check that the file is of the correct format
+ if (selectedFile.getName().endsWith(".mp3")) {
+ // If the file already exists, ask the user if they want to replace it
+ if (selectedFile.exists()) {
+ int replaceChoice = JOptionPane.showConfirmDialog(this,
+ "The file that you have selected already exists, would you like to replace it?",
+ "Confirm Replace", JOptionPane.YES_NO_CANCEL_OPTION);
+ // If the users didn't agree to replace the file then exit the method
+ if (replaceChoice != JOptionPane.YES_OPTION) {
+ return;
+ }
+ }
+
+ // Split the track at the specified points and write the output to the specified
+ // file
+ this.musicPlayer.splitCurrentTrackAndWriteToFile(selectedFile, (splitChoice == 0) ? true : false);
+ } else {
+ JOptionPane.showMessageDialog(this, "Unable to perform split. Export file must of mp3 format.",
+ "Incorrect Export Format", JOptionPane.ERROR_MESSAGE);
+ }
+ }
+ }
+
+ /**
+ * Raises an error dialog informing the user of a resource not being found.
+ *
+ * @param The name of the resource that could not be found.
+ */
+ public void raiseFileNotFoundWarning(String fileName) {
+ // The error message to be displayed to the user
+ String message;
+
+ if (fileName.endsWith(".wpl")) {
+ message = "The playlist or a track within the following playlist file could not be found:";
+ } else {
+ message = "The following file could not be found:";
+ }
+ // Display the error message dialog
+ JOptionPane.showMessageDialog(this, message + "\n" + fileName, "File Not Found", JOptionPane.ERROR_MESSAGE);
+ }
+
+ /**
+ * Displays a popup containing the release notes
+ */
+ private void displayReleaseNotes() {
+ // Fetch the text from the release notes file
+ String releaseNotes = new String();
+ try {
+ InputStream input = getClass().getResourceAsStream("/resources/release notes.txt");
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(input));
+
+ StringBuilder sb = new StringBuilder();
+
+ String line = br.readLine();
+
+ while (line != null) {
+ sb.append(line);
+ sb.append(System.lineSeparator());
+ line = br.readLine();
+ }
+ releaseNotes = sb.toString();
+
+ br.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // The text pane object containing the release notes
+ JTextPane releaseNotesTextPane = new JTextPane();
+ releaseNotesTextPane.setPreferredSize(new Dimension(350, 600));
+ releaseNotesTextPane.setFont(new Font(Font.SANS_SERIF, 0, 12));
+ releaseNotesTextPane.setText(releaseNotes);
+ releaseNotesTextPane.setEditable(false);
+ releaseNotesTextPane.setCaretPosition(0);
+
+ JScrollPane releaseNotesScrollPane = new JScrollPane(releaseNotesTextPane);
+ releaseNotesScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+ releaseNotesScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
+
+ // Display a popup containing the release notes text pane
+ JOptionPane.showMessageDialog(this, releaseNotesScrollPane, "Release Notes", JOptionPane.PLAIN_MESSAGE);
+ }
+
+ /**
+ * Closes the program.
+ */
+ public void exit() {
+ // If the current playlist isn't saved then prompt the user to save it.
+ if (!this.playlist.isSaved()) {
+ // Ask the user if they want to save the current playlist
+ int saveDialog = JOptionPane.showConfirmDialog(this,
+ "Would you like to save the current playlist before exiting?");
+
+ // If the user confirmed the dialog
+ if (saveDialog == JOptionPane.YES_OPTION) {
+ // Save the current playlist
+ boolean saveSuccessful = this.save();
+ // If the save was unsuccessful then exit this method without exiting
+ if (!saveSuccessful) {
+ return;
+ }
+ }
+ // If the user cancelled the popup then cancelled the exiting sequence
+ else if (saveDialog == JOptionPane.CANCEL_OPTION || saveDialog == JOptionPane.CLOSED_OPTION) {
+ return;
+ }
+ }
+ // Save the user defaults file
+ this.userDefaults.saveDefaultsToFile();
+ // Exit the program
+ System.exit(0);
+ }
+
+ /**
+ * Adds a document listener to the document of a textfield so that a change to
+ * the contents of the textfield calls the textFieldChanged method.
+ *
+ * @param textField - The JTextField to add the listener to.
+ */
+ public void addChangeListener(JTextField textField) {
+ // Create the document listener
+ DocumentListener dl = new DocumentListener() {
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ textFieldChange(textField);
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ textFieldChange(textField);
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ textFieldChange(textField);
+ }
+ };
+ // Add the document listener to the text field's document
+ textField.getDocument().addDocumentListener(dl);
+ }
+
+ /**
+ * Called when the author or playlist title text fields are edited.
+ */
+ private void textFieldChange(JTextField source) {
+ // Update the corresponding playlist value depending up the source of the method
+ // call
+ if (source == this.playlistNameTextField) {
+ this.playlist.setName(this.playlistNameTextField.getText());
+ } else if (source == this.playlistAuthorTextField) {
+ this.playlist.setAuthor(this.playlistAuthorTextField.getText());
+
+ }
+ }
+}
diff --git a/src/com/pe/Song.java b/src/com/pe/Song.java
new file mode 100644
index 0000000..8c6cb73
--- /dev/null
+++ b/src/com/pe/Song.java
@@ -0,0 +1,110 @@
+package com.pe;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import javax.imageio.ImageIO;
+import com.mpatric.mp3agic.ID3v2;
+import com.mpatric.mp3agic.InvalidDataException;
+import com.mpatric.mp3agic.Mp3File;
+import com.mpatric.mp3agic.UnsupportedTagException;
+
+/**
+ * Represents a song.
+ *
+ * @author olly.rowe
+ */
+public class Song
+{
+ // The file associated with this song
+ private File file;
+ // Song title
+ private String title;
+ // The artist
+ private String artist;
+ // The album
+ private String album;
+ // Length in milliseconds
+ private long length;
+ // The album art
+ private BufferedImage albumArt;
+
+ /**
+ * Class Constructor.
+ *
+ * @param path - The string value of the absolute path to the .mp3 file
+ */
+ public Song(String path) throws FileNotFoundException
+ {
+ // Set the file
+ this.file = new File(path);
+ // Attempt to extract metadata from the file
+ try
+ {
+ // Utilise MP3agic module to read the ID3v2 tag
+ Mp3File song = new Mp3File(this.getFile().getAbsolutePath());
+
+ if (song.hasId3v2Tag())
+ {
+ // Fetch the ID3v2 tag
+ ID3v2 id3v2tag = song.getId3v2Tag();
+ // Fetch and assign values from the ID3v2 tag
+ this.title = id3v2tag.getTitle();
+ this.artist = id3v2tag.getArtist();
+ this.album = id3v2tag.getAlbum();
+ this.length = song.getLengthInSeconds();
+ // Fetch the album image data
+ byte[] imageData = id3v2tag.getAlbumImage();
+ // Convert the bytes to an image if the data has been found
+ if (imageData != null)
+ {
+ this.albumArt = ImageIO.read(new ByteArrayInputStream(imageData));
+ }
+ }
+ }
+ catch (FileNotFoundException e)
+ {
+ throw new FileNotFoundException();
+ }
+ catch (IOException | InvalidDataException | UnsupportedTagException e)
+ {
+ e.printStackTrace();
+ }
+ // Assign default values for variables that have not yet been set
+ this.title = (this.title == null) ? (this.file.getName().substring(0, this.file.getName().length() - 4)) : this.title;
+ this.artist = (this.artist == null) ? "unknown artist" : this.artist;
+ this.album = (this.album == null) ? "unknown album" : this.album;
+ }
+
+ public String getTitle()
+ {
+ return this.title;
+ }
+
+ public String getArtist()
+ {
+ return this.artist;
+ }
+
+ public String getAlbum()
+ {
+ return this.album;
+ }
+
+ public long getLength()
+ {
+ return this.length;
+ }
+
+ public File getFile()
+ {
+ return this.file;
+ }
+
+ public BufferedImage getAlbumArt()
+ {
+ return this.albumArt;
+ }
+}
diff --git a/src/com/pe/UserDefaults.java b/src/com/pe/UserDefaults.java
new file mode 100644
index 0000000..ab6f508
--- /dev/null
+++ b/src/com/pe/UserDefaults.java
@@ -0,0 +1,179 @@
+package com.pe;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.PrintWriter;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+/**
+ * Class that creates and interacts with defaults.conf file that stores useful user default values.
+ *
+ * @author olly.rowe
+ *
+ */
+public class UserDefaults
+{
+ // The defaults.conf file name
+ private static final String FILE_NAME = "defaults.conf";
+
+ // The defaults file
+ private File file;
+
+ // The default directory for the 'Browse for Music' section
+ private String defaultBrowserDir;
+
+ // The default directoy when browsing to open a playlist
+ private String defaultOpenPlaylistDir;
+
+ /**
+ * Class Constructor. Will detect existing defaults.conf file and read it.
+ */
+ public UserDefaults()
+ {
+ // Initialise the defaults file
+ this.file = new File(FILE_NAME);
+
+ // If the file exists, then attempt to parse it
+ if (this.file.exists())
+ {
+ try
+ {
+ DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
+ Document doc = docBuilder.parse(this.file);
+
+ Node defaultBrowserDirNode = doc.getElementsByTagName("defaultBrowserDir").item(0);
+
+ // Set the default browser dir variable if the text content isn't empty
+ this.defaultBrowserDir = (defaultBrowserDirNode.getTextContent().equals("")) ? null : defaultBrowserDirNode.getTextContent();
+
+ Node defaultOpenPlaylistDirNode = doc.getElementsByTagName("defaultOpenPlaylistDir").item(0);
+
+ // Set the default open playlist dir variable if the text content isn't empty
+ this.defaultOpenPlaylistDir = (defaultOpenPlaylistDirNode.getTextContent().equals("")) ? null : defaultOpenPlaylistDirNode.getTextContent();
+ }
+ catch (NullPointerException e)
+ {
+ // If a null pointer error is raised then the file is missing an item of data, ignore this
+ return;
+ }
+ catch (ParserConfigurationException | SAXException | IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Getter for default browser directory string value.
+ *
+ * @return The String value of the default browser directory.
+ */
+ public String getDefaultBrowserDir()
+ {
+ return this.defaultBrowserDir;
+ }
+
+ /**
+ * Setter for the default browser directory.
+ *
+ * @param defaultBrowserDir - The string value of the path of the default browser directory.
+ */
+ public void setDefaultBrowserDir(String path)
+ {
+ this.defaultBrowserDir = path;
+ }
+
+ /**
+ * Getter for the default open playlist directory string value.
+ *
+ * @return The String value of the default directory for opening a playlist.
+ */
+ public String getDefaultOpenPlaylistDir()
+ {
+ return this.defaultOpenPlaylistDir;
+ }
+
+ /**
+ * Setter for the default open playlist directory.
+ *
+ * @param path - The string value of the path of the default browser directory.
+ */
+ public void setDefaultOpenPlaylistDir(String path)
+ {
+ this.defaultOpenPlaylistDir = path;
+ }
+
+ /**
+ * Saves the current defaults to the defaults.conf file.
+ */
+ public void saveDefaultsToFile()
+ {
+ // Attempt to parse defaults.conf file
+ try
+ {
+ // Adapted from: https://examples.javacodegeeks.com/core-java/xml/parsers/documentbuilderfactory/create-xml-file-in-java-using-dom-parser-example/
+ DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder documentBuilder = documentFactory.newDocumentBuilder();
+ Document document = documentBuilder.newDocument();
+
+ Element defaultsElement = document.createElement("Defaults");
+
+ document.appendChild(defaultsElement);
+
+ Element defaultBrowserDirElement = document.createElement("defaultBrowserDir");
+
+ if (this.defaultBrowserDir != null)
+ {
+ defaultBrowserDirElement.appendChild(document.createTextNode(this.defaultBrowserDir));
+ }
+
+ defaultsElement.appendChild(defaultBrowserDirElement);
+
+ Element defaultOpenPlaylistDirElement = document.createElement("defaultOpenPlaylistDir");
+
+ if (this.defaultOpenPlaylistDir != null)
+ {
+ defaultOpenPlaylistDirElement.appendChild(document.createTextNode(this.defaultOpenPlaylistDir));
+ }
+
+ defaultsElement.appendChild(defaultOpenPlaylistDirElement);
+
+ // Create new transformation objects
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ Transformer transformer = transformerFactory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ DOMSource domSource = new DOMSource(document);
+ // Create a new StringWriter object to output the result to
+ StringWriter outWriter = new StringWriter();
+ // The stream result, outputting to the String Writer
+ StreamResult streamResult = new StreamResult(outWriter);
+ // Transform the DOM Object to an XML File
+ transformer.transform(domSource, streamResult);
+ StringBuffer sb = outWriter.getBuffer();
+
+ // Write the output to the file
+ PrintWriter pw = new PrintWriter(this.file.getAbsolutePath());
+ pw.println(sb.toString());
+ pw.close();
+ }
+ catch (ParserConfigurationException | TransformerException | FileNotFoundException e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/com/pe/audio/MusicPlayer.java b/src/com/pe/audio/MusicPlayer.java
new file mode 100644
index 0000000..e9de94a
--- /dev/null
+++ b/src/com/pe/audio/MusicPlayer.java
@@ -0,0 +1,556 @@
+package com.pe.audio;
+
+import com.pe.Song;
+import com.pe.Playlist;
+
+import java.awt.Image;
+import java.awt.Point;
+import java.awt.SystemColor;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Set;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+import javax.swing.plaf.basic.BasicSliderUI;
+
+import javazoom.jl.decoder.JavaLayerException;
+import javazoom.jl.player.Player;
+
+/**
+ * Represent the music player component.
+ *
+ * @author olly.rowe
+ */
+@SuppressWarnings("serial")
+public class MusicPlayer extends JPanel implements ActionListener
+{
+ // The javazoom player object
+ private Player player;
+
+ // The thread object that the player will run within
+ private Thread playerThread;
+
+ // The member variables associated with the player
+ private FileInputStream stream;
+ private long totalSongLength;
+
+ // Whether the player is currently in a paused state
+ private volatile boolean isPaused = false;
+
+ // The song that is currently playing
+ private Song currentSong;
+
+ // The playlist being played
+ private Playlist playlist;
+
+ // The song title text field
+ private JTextField txtSongTitle;
+
+ // The button components
+ private JButton btnSkipBackSong;
+ private JButton btnPlayPause;
+ private JButton btnSkipForwardSong;
+
+ // The song progress bar
+ private JSlider progressBar;
+
+ // Play icon
+ private final Image PLAY_ICON = (new ImageIcon(getClass().getResource("/images/play.png"))).getImage().getScaledInstance(12, 12, Image.SCALE_SMOOTH);
+ // Pause icon
+ private final Image PAUSE_ICON = (new ImageIcon(getClass().getResource("/images/pause.png"))).getImage().getScaledInstance(12, 12, Image.SCALE_SMOOTH);
+
+ // The album art JPanel component
+ private JLabel albumArtLabel;
+
+ // The default album art cover
+ private final Image DEFAULT_ALBUM_ART_ICON = (new ImageIcon(getClass().getResource("/images/default album art.png"))).getImage().getScaledInstance(70, 70, Image.SCALE_SMOOTH);
+
+ /**
+ * Class Constructor.
+ *
+ * @param playlist - The playlist of songs to be played.
+ * @param albumArtPanel - The JLabel where the album art will appear.
+ */
+ public MusicPlayer(Playlist playlist, JLabel albumArtLabel)
+ {
+ super();
+
+ // Set the playlist
+ this.playlist = playlist;
+ // Set the album art label
+ this.albumArtLabel = albumArtLabel;
+
+ // Remove the layout
+ this.setLayout(null);
+
+ // Set the default album art icon
+ this.albumArtLabel.setIcon(new ImageIcon(DEFAULT_ALBUM_ART_ICON));
+
+ // Set up skip backwards button
+ Image skipBackwardsIcon = (new ImageIcon(getClass().getResource("/images/skip backwards.png"))).getImage().getScaledInstance(15, 15, Image.SCALE_SMOOTH);
+ btnSkipBackSong = new JButton(new ImageIcon(skipBackwardsIcon));
+ btnSkipBackSong.addActionListener(this);
+ btnSkipBackSong.setBounds(10, 20, 80, 23);
+ add(btnSkipBackSong);
+
+ // Set up play / pause button
+ btnPlayPause = new JButton(new ImageIcon(PLAY_ICON));
+ btnPlayPause.addActionListener(this);
+ btnPlayPause.setBounds(136, 20, 58, 23);
+ add(btnPlayPause);
+
+ // Set up skip backwards button
+ Image skipForwardsIcon = (new ImageIcon(getClass().getResource("/images/skip forwards.png"))).getImage().getScaledInstance(15, 15, Image.SCALE_SMOOTH);
+ btnSkipForwardSong = new JButton(new ImageIcon(skipForwardsIcon));
+ btnSkipForwardSong.addActionListener(this);
+ btnSkipForwardSong.setBounds(240, 20, 80, 23);
+ add(btnSkipForwardSong);
+
+ // Set up the song title and artist text field
+ txtSongTitle = new JTextField();
+ txtSongTitle.setBorder(BorderFactory.createEmptyBorder());
+ txtSongTitle.setHorizontalAlignment(SwingConstants.CENTER);
+ txtSongTitle.setEditable(false);
+ txtSongTitle.setBackground(SystemColor.menu);
+ txtSongTitle.setBounds(10, 0, 310, 18);
+ add(txtSongTitle);
+
+ // Set up the song progress bar. Removes listeners that disallow the user to seek a position on the slider by clicking at a certain point.
+ progressBar = new JSlider() {
+ {
+ MouseListener[] listeners = getMouseListeners();
+ for (MouseListener l : listeners)
+ removeMouseListener(l); // remove UI-installed TrackListener
+ final BasicSliderUI ui = (BasicSliderUI) getUI();
+ BasicSliderUI.TrackListener tl = ui.new TrackListener()
+ {
+ @Override
+ public void mouseReleased(MouseEvent e)
+ {
+ Point p = e.getPoint();
+ int value = ui.valueForXPosition(p.x);
+ setValue(value);
+ if (!isPaused)
+ {
+ pause();
+ setValue(value);
+ resume();
+ }
+ }
+
+ @Override public boolean shouldScroll(int dir) {
+ return false;
+ }
+ };
+ addMouseListener(tl);
+ }
+ };
+
+ // Set the value to 0 by default
+ progressBar.setValue(0);
+
+ progressBar.setBounds(10, 43, 310, 24);
+ add(progressBar);
+ }
+
+ /**
+ * Button Action listener handler.
+ */
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ // Ensure that a playlist has been loaded and isn't empty
+ Object source = e.getSource();
+
+ if (source == btnSkipBackSong)
+ {
+ skipBackSong();
+ }
+ else if (source == btnPlayPause)
+ {
+ playPauseSong();
+ }
+ else if (source == btnSkipForwardSong)
+ {
+ skipForwardSong();
+ }
+ }
+
+ /**
+ * Toggles between playing and pausing the current song.
+ */
+ public void playPauseSong()
+ {
+ if (this.player != null)
+ {
+ if (this.isPaused)
+ {
+ this.resume();
+ }
+ else
+ {
+ this.pause();
+ }
+ }
+ else
+ {
+ // Check that the playlist isn't empty
+ if (this.playlist.getSong(0) != null)
+ {
+ // If the player is not yet initialised, default the current song to the first one in the playlist
+ this.currentSong = this.playlist.getSongs().get(0);
+ // Play the song from the start
+ this.playSong(0);
+ }
+ }
+ }
+
+ /**
+ * Plays the current song.
+ *
+ * @param startPoint - The point within the song to start playing form.
+ */
+ private void playSong(long startPoint)
+ {
+ // Check that there is a current song to play
+ if (this.currentSong != null)
+ {
+ this.isPaused = false;
+
+ if (player != null)
+ {
+ player.close();
+ player = null;
+ }
+ // Attempt to play the audio file
+ try
+ {
+ this.stream = new FileInputStream(this.currentSong.getFile());
+
+ this.totalSongLength = this.stream.available();
+
+ this.progressBar.setMaximum((int) this.totalSongLength);
+
+ this.stream.skip(startPoint);
+
+ player = new Player(this.stream);
+
+ this.playerThread = new Thread("Music Player")
+ {
+ public void run()
+ {
+ try
+ {
+ updateGUI();
+
+ btnPlayPause.setIcon(new ImageIcon(PAUSE_ICON));
+
+ player.play();
+
+ // Once the player is complete, skip to the next song in the playlist
+ if (player != null && player.isComplete())
+ {
+ skipForwardSong();
+ }
+ }
+ catch (JavaLayerException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ this.playerThread.start();
+
+ // Get all currently running threads
+ Set threadSet = Thread.getAllStackTraces().keySet();
+
+ // End method if there is currently a progress bar updater thread running
+ for (Thread thread : threadSet)
+ {
+ if (thread.getName().equals("Progress Bar Updater"))
+ {
+ return;
+ }
+ }
+
+ // Create a new thread to update the progress bar
+ Thread updateProgressBarThread = new Thread("Progress Bar Updater") {
+ public void run()
+ {
+ // maybe check if one of these threads is already running as they can double up when you navigate through a song using progress bar
+ while (!this.isInterrupted())
+ {
+ // If player's state changes before song ends then catch the null pointer exception
+ try
+ {
+ if (player.isComplete() || isPaused)
+ {
+ break;
+ }
+
+ // Update the progress bar
+ updateProgressBar();
+
+ Thread.sleep(100);
+ }
+ catch (NullPointerException | InterruptedException e)
+ {
+ break;
+ }
+ }
+ }
+ };
+
+ updateProgressBarThread.start();
+ }
+ catch (JavaLayerException | IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Plays the current song from the beginning.
+ */
+ private void playSong()
+ {
+ this.playSong(this.progressBar.getValue());
+ }
+
+ /**
+ * Pauses the song currently being played in the player.
+ */
+ private void pause()
+ {
+ this.btnPlayPause.setIcon(new ImageIcon(PLAY_ICON));
+
+ if (player != null)
+ {
+ this.player.close();
+ }
+ this.isPaused = true;
+ }
+
+ /**
+ * Resumes a paused song in the player.
+ */
+ private void resume()
+ {
+ // Play the song from pause location
+ playSong(this.progressBar.getValue());
+ }
+
+ /**
+ * Skips the song currently being played back to the previous one in the playlist.
+ */
+ private void skipBackSong()
+ {
+ // Check that the first song in the playlist isn't currently being played
+ if (this.currentSong != this.playlist.getSong(0) && this.playlist.getSize() > 0)
+ {
+ // Change the current song to the previous on in the playlist
+ this.currentSong = this.playlist.getSong(this.playlist.getIndexOf(this.currentSong) - 1);
+ }
+ // Play the current song from the start
+ this.playSong(0);
+ }
+
+ /**
+ * Skips the song currently being played forward to the next one in the playlist.
+ */
+ public void skipForwardSong()
+ {
+ // Check that the playlist has a final song before doing anything
+ if (this.playlist.getLastSong() != null)
+ {
+ // Check that the final song in the playlist isn't currently being played
+ if (this.currentSong != this.playlist.getLastSong())
+ {
+ // Update the current song to the previous one in the playlist
+ this.currentSong = this.playlist.getSong(this.playlist.getIndexOf(this.currentSong) + 1);
+ // Play the next song
+ this.playSong(0);
+ }
+ // Otherwise, go into a paused state
+ else
+ {
+ // Enter paused state
+ this.pause();
+ // Reset the progress bar
+ this.progressBar.setValue(0);
+ }
+ }
+ }
+
+ /**
+ * Splits the current song at the current progress into two.
+ */
+ public void splitCurrentTrackAndWriteToFile(File outputFile, boolean outputPreSplitPoint)
+ {
+ // Check that there is a current track
+ if (this.currentSong != null)
+ {
+ try
+ {
+ // Create a new temporary input stream for the current song
+ FileInputStream tempInputStream = new FileInputStream(this.currentSong.getFile());
+
+ int startPoint, endPoint;
+
+ // Calculate the start and end points based upon argument
+ if (outputPreSplitPoint)
+ {
+ startPoint = 0;
+ endPoint = this.progressBar.getValue();
+ }
+ else
+ {
+ startPoint = this.progressBar.getValue();
+ endPoint = (int) this.totalSongLength;
+ }
+
+ // Skip to the start point
+ tempInputStream.skip((long) startPoint);
+
+ // Create a new output stream to the output file
+ FileOutputStream outputStream = new FileOutputStream(outputFile);
+
+ // The number of remaining bytes to be read and outputted
+ int remainingBytes = endPoint - startPoint;
+
+ // Create the buffer to be written to
+ byte[] buffer = new byte[remainingBytes];
+
+ // Populate contents of the buffer from the input stream
+ tempInputStream.read(buffer);
+ // Write the contents of the buffer to the output stream
+ outputStream.write(buffer);
+
+ // Close the opened streams
+ tempInputStream.close();
+ outputStream.close();
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Updates the current playlist associated with this music player with a new one.
+ *
+ * @param playlist - The new playlist.
+ */
+ public void updatePlaylist(Playlist playlist)
+ {
+ // Update the playlist member variable
+ this.playlist = playlist;
+
+ if (playlist.getSize() > 0)
+ {
+ // Get the first song from the playlist
+ Song firstSong = playlist.getSongs().get(0);
+
+ // Set the current song
+ this.currentSong = firstSong;
+
+ }
+
+ // Update the GUI components
+ this.updateGUI();
+ }
+
+ /**
+ * Updates the progress bar with the current song progress.
+ */
+ public void updateProgressBar()
+ {
+ int progress;
+ try
+ {
+ // Calculate progress as the total length of the song minus the length of song left
+ progress = (int) (this.totalSongLength - this.stream.available());
+ }
+ catch (IOException e)
+ {
+ return;
+ }
+ // Update the progress bar value
+ this.progressBar.setValue(progress);
+ }
+
+ /**
+ * Updates the GUI components with the details of the current song.
+ */
+ public void updateGUI()
+ {
+ // Update the album art
+ try
+ {
+ this.albumArtLabel.setIcon(new ImageIcon(new ImageIcon(this.currentSong.getAlbumArt()).getImage().getScaledInstance(70, 70, Image.SCALE_SMOOTH)));
+ }
+ catch (NullPointerException e)
+ {
+ this.albumArtLabel.setIcon(new ImageIcon(DEFAULT_ALBUM_ART_ICON));
+ }
+
+ // Update the song title and artist text field with the details of the current song
+ this.txtSongTitle.setText(this.currentSong.getTitle() + " - " + this.currentSong.getArtist());
+ }
+
+ /**
+ * Sets the current song. Stops the current song from playing and resets the progress bar.
+ *
+ * @param the song to be set as the current song.
+ */
+ public void setCurrentSong(Song song)
+ {
+ // The the current song
+ this.currentSong = song;
+ // Pause the song that is currently playing
+ this.pause();
+ // Reset the progress bar
+ this.progressBar.setValue(0);
+ // Update the max value
+ try
+ {
+ // Open a new temporary stream
+ FileInputStream tempStream = new FileInputStream(this.currentSong.getFile());
+ // Set the max value
+ this.progressBar.setMaximum((int) tempStream.available());
+ // Close the stream
+ tempStream.close();
+ }
+ catch (IOException e)
+ {
+ return;
+ }
+ }
+
+ /**
+ * Gets the current song.
+ *
+ * @return the current song.
+ */
+ public Song getCurrentSong()
+ {
+ return this.currentSong;
+ }
+}
\ No newline at end of file
diff --git a/src/com/pe/utils/FileNode.java b/src/com/pe/utils/FileNode.java
new file mode 100644
index 0000000..4647a5d
--- /dev/null
+++ b/src/com/pe/utils/FileNode.java
@@ -0,0 +1,84 @@
+package com.pe.utils;
+
+import java.io.File;
+import javax.swing.Icon;
+import javax.swing.tree.DefaultMutableTreeNode;
+
+/**
+ * Represents a node within the file tree.
+ */
+@SuppressWarnings("serial")
+public class FileNode extends DefaultMutableTreeNode
+{
+ // The corresponding file
+ private File file;
+
+ // Boolean value identifying whether this node has yet been mapped
+ private boolean isMapped = false;
+
+ // The icon associated with this node that is called when rendering
+ private Icon icon;
+
+ /**
+ * Class constructor.
+ *
+ * @param file - The associated file to this node.
+ */
+ public FileNode(File file)
+ {
+ // Call to super constructor. Pass either the name of the file or, for the root, pass the path
+ super(file.getName().equals("") ? file.getAbsolutePath() : file.getName());
+
+ this.file = file;
+ }
+
+ /**
+ * Gets the file.
+ *
+ * @return The corresponding file to this node.
+ */
+ public File getFile()
+ {
+ return this.file;
+ }
+
+ /**
+ * Sets the state of isMapped.
+ *
+ * @param isMapped Whether this node is mapped.
+ */
+ public void setMapped(boolean isMapped)
+ {
+ this.isMapped = isMapped;
+ }
+
+ /**
+ * Returns whether this node is yet mapped.
+ *
+ * @return State of isMapped attribute.
+ */
+ public boolean isMapped()
+ {
+ return this.isMapped;
+ }
+
+ /**
+ * Gets the associated icon.
+ *
+ * @return the associated icon.
+ */
+ public Icon getIcon()
+ {
+ return this.icon;
+ }
+
+ /**
+ * Sets the associated icon.
+ *
+ * @param icon - the icon to be set.
+ */
+ public void setIcon(Icon icon)
+ {
+ this.icon = icon;
+ }
+}
\ No newline at end of file
diff --git a/src/com/pe/utils/FileTree.java b/src/com/pe/utils/FileTree.java
new file mode 100644
index 0000000..4734f31
--- /dev/null
+++ b/src/com/pe/utils/FileTree.java
@@ -0,0 +1,277 @@
+package com.pe.utils;
+
+import java.awt.Component;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JTree;
+import javax.swing.UIManager;
+import javax.swing.event.TreeExpansionEvent;
+import javax.swing.event.TreeExpansionListener;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import javax.swing.tree.DefaultTreeModel;
+
+/**
+ * FileTree.java
+ *
+ * Represents a JTree that displays a given file system.
+ *
+ * Threading has been utilised to map the file system using a breadth-first search.
+ * Nodes can also be mapped on an as-needed bases to avoid long loading times.
+ */
+@SuppressWarnings("serial")
+public class FileTree extends JTree implements TreeExpansionListener
+{
+ // The array of file types to be displayed in the tree
+ private ArrayList fileTypesToShow;
+
+ /**
+ * Class constructor.
+ *
+ * @param directory - String value of the path of the directory to be set.
+ * @param fileTypesToShow - array of strings indicating file types to be included.
+ */
+ public FileTree(String directory, List fileTypesToShow)
+ {
+ super(new FileNode(new File(directory)));
+
+ this.fileTypesToShow = new ArrayList(fileTypesToShow);
+
+ // Map the root node
+ this.mapNodes((FileNode) this.getModel().getRoot());
+
+ // Add tree expansion listener
+ this.addTreeExpansionListener(this);
+
+ // Set the cell renderer for assignment of node icons
+ this.setCellRenderer(new DefaultTreeCellRenderer()
+ {
+ @Override
+ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus)
+ {
+ super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+
+ FileNode node = (FileNode) value;
+
+ // If an icon has been set for this node then set it
+ if (node.getIcon() != null)
+ {
+ setIcon(node.getIcon());
+ }
+ return this;
+ }
+ });
+ }
+
+ /**
+ * Alternative Class Constructor. By default, will display all file types.
+ *
+ * @param directory - String value of the path of the directory to be set.
+ */
+ public FileTree(String directory)
+ {
+ this(directory, null);
+ }
+
+ /**
+ * Maps a node, discovers and creates child nodes.
+ *
+ * @param parent - the node to be mapped.
+ */
+ private void mapNodes(FileNode node)
+ {
+ // List all child files / directories within parent node
+ File[] children = node.getFile().listFiles();
+
+ if (children != null)
+ {
+ // Iterate through each child
+ for (File child : children)
+ {
+ if (child.isFile())
+ {
+ if (!this.isValidType(child))
+ {
+ continue;
+ }
+ }
+ // Create a new node
+ FileNode childNode = new FileNode(child);
+ // Add the new node to the parent node
+ node.add(childNode);
+
+ // If this child node is a directory
+ if (child.isDirectory())
+ {
+ // Set the icon of the child not to the default folder
+ childNode.setIcon(UIManager.getIcon("Tree.openIcon"));
+ }
+ }
+ }
+ }
+
+ /**
+ * Fired upon expansion of a node. Maps the children of the children of the expanded node.
+ *
+ * @param e - The event object
+ */
+ @Override
+ public void treeExpanded(TreeExpansionEvent e)
+ {
+ // Fetch the last selected node
+ FileNode node = (FileNode) e.getPath().getLastPathComponent();
+
+ // Check if the node is not yet mapped
+ if (!node.isMapped())
+ {
+ // Iterate through each child node
+ for (int i = 0; i < node.getChildCount(); i++)
+ {
+ FileNode child = (FileNode) node.getChildAt(i);
+ // Map the child's children
+ this.mapNodes(child);
+ }
+ }
+ // Set the node to mapped
+ node.setMapped(true);
+ }
+
+ /**
+ * Fired upon collapsing of a node.
+ *
+ * @param e - The event object
+ */
+ @Override
+ public void treeCollapsed(TreeExpansionEvent e)
+ {
+ return;
+ }
+
+ /**
+ * Changes the directory.
+ *
+ * @param directory - String value of the path of the directory to be set.
+ */
+ public void setDirectory(String directory)
+ {
+ // Get the tree model
+ DefaultTreeModel model = (DefaultTreeModel) this.getModel();
+ // Set the new root node
+ model.setRoot(new FileNode(new File(directory)));
+ // Refresh the GUI
+ model.reload();
+ // Map the child nodes of the new root
+ this.mapNodes((FileNode) this.getModel().getRoot());
+ }
+
+ /**
+ * Check that the file's type is allow by the file type filters. If there are not filters then it shall return true.
+ *
+ * @return - the Boolean value of whether the file's type is valid or not.
+ */
+ private boolean isValidType(File file)
+ {
+ if (this.fileTypesToShow != null)
+ {
+ // Iterate through each valid file type
+ for (String fileType : this.fileTypesToShow)
+ {
+ // If the file type is valid
+ if (file.getName().endsWith(fileType))
+ {
+ return true;
+ }
+ }
+ }
+ // If the file types to show array was null or the for loop did not return true at any point then return false
+ return false;
+ }
+}
+
+/**
+ * FilteredFileTree.
+ *
+ * An extension of the FileTree class that allows the user to pass an array list of String that specify the file types to be displayed in the file tree.
+ *
+ * The performance of this current implementation is low and requires threading to be implemented to fix this issue.
+ */
+@SuppressWarnings("serial")
+class FilteredFileTree extends FileTree
+{
+ // Filters for any specific file types to be displayed
+ private ArrayList fileTypesFilter;
+
+ /**
+ * Class Constructor.
+ *
+ * @param directory - String value of the path of the directory to be set.
+ * @param fileTypesFilter - String array containing the types of files to be shown in the file tree.
+ */
+ public FilteredFileTree(String directory, ArrayList fileTypesFilter)
+ {
+ super(directory);
+
+ this.fileTypesFilter = fileTypesFilter;
+ }
+
+ /**
+ * Maps the child node into this current tree. Filtering out unspecified file types.
+ *
+ * @param parent - the parent file node.
+ */
+ @SuppressWarnings("unused")
+ private void mapNodes(FileNode parent)
+ {
+ // List all child files / directories within parent node
+ File[] children = parent.getFile().listFiles();
+
+ if (children != null)
+ {
+ // Iterate through each child
+ for (File child : children)
+ {
+ // If the child is a file then perform type validation check
+ if (child.isFile())
+ {
+ // Ensure that the child is of one of the types specified in the type filters. If not then continue for loop.
+ if (!isValidType(child))
+ {
+ continue;
+ }
+ }
+ // Create a new node
+ FileNode node = new FileNode(child);
+ // Add the new node to the parent node
+ parent.add(node);
+ }
+ }
+ }
+
+ /**
+ * Check that the file's type is allow by the file type filters. If there are not filters then it shall return true.
+ *
+ * @return - the Boolean value of whether the file's type is valid or not.
+ */
+ private boolean isValidType(File file)
+ {
+ if (this.fileTypesFilter == null)
+ {
+ return true;
+ }
+ else
+ {
+ // Iterate through each valid file type
+ for (String fileType : this.fileTypesFilter)
+ {
+ // If the file type is valid
+ if (file.getName().endsWith(fileType))
+ {
+ return true;
+ }
+ }
+ // If the for loop did not return true at any point then the file type is invalid.
+ return false;
+ }
+ }
+}
diff --git a/src/com/pe/utils/SetupWorker.java b/src/com/pe/utils/SetupWorker.java
new file mode 100644
index 0000000..1e18bbd
--- /dev/null
+++ b/src/com/pe/utils/SetupWorker.java
@@ -0,0 +1,31 @@
+package com.pe.utils;
+
+import javax.swing.SwingWorker;
+
+/**
+ * SetupWorker.java
+ *
+ * Handles set-up processes in the background.
+ *
+ * @author olly.rowe
+ */
+public class SetupWorker extends SwingWorker
+{
+
+ /**
+ * Class Constructor.
+ */
+ public SetupWorker()
+ {
+ super();
+ }
+
+ /**
+ * Inherited SwingWorker method. Called upon .execute()
+ */
+ @Override
+ protected Object doInBackground() throws Exception
+ {
+ return null;
+ }
+}
diff --git a/src/images/arrow down.png b/src/images/arrow down.png
new file mode 100644
index 0000000..644b44b
Binary files /dev/null and b/src/images/arrow down.png differ
diff --git a/src/images/arrow up.png b/src/images/arrow up.png
new file mode 100644
index 0000000..f3b227f
Binary files /dev/null and b/src/images/arrow up.png differ
diff --git a/src/images/default album art.png b/src/images/default album art.png
new file mode 100644
index 0000000..95eda7c
Binary files /dev/null and b/src/images/default album art.png differ
diff --git a/src/images/icon.png b/src/images/icon.png
new file mode 100644
index 0000000..469d920
Binary files /dev/null and b/src/images/icon.png differ
diff --git a/src/images/music note.png b/src/images/music note.png
new file mode 100644
index 0000000..599e02d
Binary files /dev/null and b/src/images/music note.png differ
diff --git a/src/images/pause.png b/src/images/pause.png
new file mode 100644
index 0000000..361b931
Binary files /dev/null and b/src/images/pause.png differ
diff --git a/src/images/play next button.png b/src/images/play next button.png
new file mode 100644
index 0000000..70e0f23
Binary files /dev/null and b/src/images/play next button.png differ
diff --git a/src/images/play.png b/src/images/play.png
new file mode 100644
index 0000000..747e191
Binary files /dev/null and b/src/images/play.png differ
diff --git a/src/images/skip backwards.png b/src/images/skip backwards.png
new file mode 100644
index 0000000..fb141ca
Binary files /dev/null and b/src/images/skip backwards.png differ
diff --git a/src/images/skip forwards.png b/src/images/skip forwards.png
new file mode 100644
index 0000000..c8b88d2
Binary files /dev/null and b/src/images/skip forwards.png differ