summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/wx/content_view.cc13
-rw-r--r--src/wx/content_view.h4
-rw-r--r--src/wx/controls.cc15
-rw-r--r--src/wx/controls.h6
-rw-r--r--src/wx/player_frame.cc391
-rw-r--r--src/wx/player_frame.h22
-rw-r--r--src/wx/playlist_controls.cc327
-rw-r--r--src/wx/playlist_controls.h38
-rw-r--r--src/wx/standard_controls.cc15
-rw-r--r--src/wx/standard_controls.h3
-rw-r--r--web/index.html63
-rw-r--r--web/sidebar.html4
12 files changed, 563 insertions, 338 deletions
diff --git a/src/wx/content_view.cc b/src/wx/content_view.cc
index 46b8c8ce9..52e8e708e 100644
--- a/src/wx/content_view.cc
+++ b/src/wx/content_view.cc
@@ -51,6 +51,9 @@ using std::string;
using std::vector;
using std::weak_ptr;
using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
using namespace dcpomatic;
@@ -62,6 +65,16 @@ ContentView::ContentView (wxWindow* parent)
AppendColumn({}, wxLIST_FORMAT_LEFT, 80);
/* annotation text */
AppendColumn({}, wxLIST_FORMAT_LEFT, 580);
+
+ Bind(wxEVT_LIST_ITEM_ACTIVATED, boost::bind(&ContentView::activated, this, _1));
+}
+
+
+void
+ContentView::activated(wxListEvent& ev)
+{
+ DCPOMATIC_ASSERT(ev.GetIndex() < static_cast<int>(_content.size()));
+ Activated(_content[ev.GetIndex()]);
}
diff --git a/src/wx/content_view.h b/src/wx/content_view.h
index 300e698f3..30284d42e 100644
--- a/src/wx/content_view.h
+++ b/src/wx/content_view.h
@@ -24,6 +24,7 @@
LIBDCP_DISABLE_WARNINGS
#include <wx/listctrl.h>
LIBDCP_ENABLE_WARNINGS
+#include <boost/signals2.hpp>
#include <vector>
@@ -39,8 +40,11 @@ public:
std::shared_ptr<Content> selected () const;
void update ();
+ boost::signals2::signal<void (std::weak_ptr<Content>)> Activated;
+
private:
void add (std::shared_ptr<Content> content);
+ void activated(wxListEvent& ev);
std::weak_ptr<Film> _film;
std::vector<std::shared_ptr<Content>> _content;
diff --git a/src/wx/controls.cc b/src/wx/controls.cc
index fce3fd7eb..2c4e92108 100644
--- a/src/wx/controls.cc
+++ b/src/wx/controls.cc
@@ -457,3 +457,18 @@ Controls::seek(int slider)
slider_moved(false);
slider_released();
}
+
+
+void
+Controls::play()
+{
+ _viewer.start();
+}
+
+
+void
+Controls::stop()
+{
+ _viewer.stop();
+}
+
diff --git a/src/wx/controls.h b/src/wx/controls.h
index 3dec6fb88..21b469bb7 100644
--- a/src/wx/controls.h
+++ b/src/wx/controls.h
@@ -62,14 +62,16 @@ public:
void set_film(std::shared_ptr<Film> film);
- virtual void play() {};
- virtual void stop() {};
+ void play();
+ void stop();
void seek(int slider);
std::shared_ptr<Film> film() const;
void back_frame();
void forward_frame();
+ virtual void playlist_changed() {}
+
protected:
virtual void started();
diff --git a/src/wx/player_frame.cc b/src/wx/player_frame.cc
index a945a5b1d..afc43b807 100644
--- a/src/wx/player_frame.cc
+++ b/src/wx/player_frame.cc
@@ -48,6 +48,7 @@
#include "lib/font_config.h"
#include "lib/job_manager.h"
#include "lib/null_log.h"
+#include "lib/show_playlist_content_store.h"
#include "lib/text_content.h"
#include "lib/update_checker.h"
#include "lib/variant.h"
@@ -59,6 +60,7 @@
LIBDCP_DISABLE_WARNINGS
#include <wx/display.h>
#include <wx/preferences.h>
+#include <wx/progdlg.h>
#include <wx/stdpaths.h>
LIBDCP_ENABLE_WARNINGS
@@ -214,9 +216,7 @@ PlayerFrame::PlayerFrame()
Bind(wxEVT_CLOSE_WINDOW, boost::bind(&PlayerFrame::close, this, _1));
- if (Config::instance()->enable_player_http_server()) {
- update_content_store();
- }
+ update_content_store();
if (Config::instance()->player_mode() == Config::PlayerMode::DUAL) {
_controls = new PlaylistControls(_overall_panel, this, _viewer);
@@ -227,6 +227,7 @@ PlayerFrame::PlayerFrame()
_viewer.set_dcp_decode_reduction(Config::instance()->decode_reduction());
_viewer.PlaybackPermitted.connect(bind(&PlayerFrame::playback_permitted, this));
_viewer.TooManyDropped.connect(bind(&PlayerFrame::too_many_frames_dropped, this));
+ _viewer.Finished.connect(boost::bind(&PlayerFrame::viewer_finished, this));
_info = new PlayerInformation(_overall_panel, _viewer);
setup_main_sizer(Config::instance()->player_mode());
#ifdef __WXOSX__
@@ -267,7 +268,7 @@ PlayerFrame::PlayerFrame()
Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_to_start, this), ID_go_to_start);
Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_to_end, this), ID_go_to_end);
- reset_film();
+ take_playlist_entry();
UpdateChecker::instance()->StateChanged.connect(boost::bind(&PlayerFrame::update_checker_state_changed, this));
setup_screen();
@@ -324,14 +325,14 @@ PlayerFrame::setup_main_sizer(Config::PlayerMode mode)
bool
PlayerFrame::playback_permitted()
{
- if (!_film || !Config::instance()->respect_kdm_validity_periods()) {
+ if (!Config::instance()->respect_kdm_validity_periods()) {
return true;
}
bool ok = true;
- for (auto i: _film->content()) {
- auto d = dynamic_pointer_cast<DCPContent>(i);
- if (d && !d->kdm_timing_window_valid()) {
+ for (auto content: _playlist) {
+ auto dcp = dynamic_pointer_cast<DCPContent>(content.first);
+ if (dcp && !dcp->kdm_timing_window_valid()) {
ok = false;
}
}
@@ -379,39 +380,30 @@ PlayerFrame::set_decode_reduction(optional<int> reduction)
void
PlayerFrame::load_dcp(boost::filesystem::path dir)
{
- DCPOMATIC_ASSERT(_film);
-
- auto film = std::make_shared<Film>(optional<boost::filesystem::path>());
-
try {
_stress.set_suspended(true);
/* Handler to set things up once the DCP has been examined */
- auto setup = [this](weak_ptr<Film> weak_film, weak_ptr<Job> weak_job, weak_ptr<Content> weak_content)
+ auto setup = [this](weak_ptr<Job> weak_job, weak_ptr<Content> weak_content)
{
auto job = weak_job.lock();
if (!job || !job->finished_ok()) {
return;
}
- auto content = weak_content.lock();
- if (!content) {
- return;
+ if (auto content = weak_content.lock()) {
+ _playlist = { make_pair(content, boost::optional<float>()) };
+ _playlist_position = 0;
+ _controls->playlist_changed();
+ take_playlist_entry();
}
- auto film = weak_film.lock();
- if (!film) {
- return;
- }
-
- film->add_content({content});
_stress.set_suspended(false);
- reset_film(film);
};
auto dcp = make_shared<DCPContent>(dir);
auto job = make_shared<ExamineContentJob>(vector<shared_ptr<Content>>{dcp}, true);
- _examine_job_connection = job->Finished.connect(boost::bind<void>(setup, weak_ptr<Film>(film), weak_ptr<Job>(job), weak_ptr<Content>(dcp)));
+ _examine_job_connection = job->Finished.connect(boost::bind<void>(setup, weak_ptr<Job>(job), weak_ptr<Content>(dcp)));
JobManager::instance()->add(job);
bool const ok = display_progress(variant::wx::dcpomatic_player(), _("Loading content"));
if (ok && report_errors_from_last_job(this)) {
@@ -436,19 +428,6 @@ PlayerFrame::load_dcp(boost::filesystem::path dir)
}
-void
-PlayerFrame::reset_film(shared_ptr<Film> film, optional<float> crop_to_ratio)
-{
- _film = film;
-
- if (!crop_to_ratio) {
- crop_to_ratio = Config::instance()->player_crop_output_ratio();
- }
-
- prepare_to_play_film(crop_to_ratio);
-}
-
-
/* _film is now something new: set up to play it */
void
PlayerFrame::prepare_to_play_film(optional<float> crop_to_ratio)
@@ -553,13 +532,9 @@ PlayerFrame::prepare_to_play_film(optional<float> crop_to_ratio)
void
PlayerFrame::set_audio_delay_from_config()
{
- if (!_film) {
- return;
- }
-
- for (auto i: _film->content()) {
- if (i->audio) {
- i->audio->set_delay(Config::instance()->player_audio_delay());
+ for (auto content: _playlist) {
+ if (content.first->audio) {
+ content.first->audio->set_delay(Config::instance()->player_audio_delay());
}
}
}
@@ -596,8 +571,11 @@ PlayerFrame::idle()
void
PlayerFrame::examine_content()
{
- DCPOMATIC_ASSERT(_film);
- auto dcp = dynamic_pointer_cast<DCPContent>(_film->content().front());
+ if (_playlist.empty()) {
+ return;
+ }
+
+ auto dcp = dynamic_pointer_cast<DCPContent>(_playlist.front().first);
DCPOMATIC_ASSERT(dcp);
dcp->examine({}, true);
@@ -761,9 +739,8 @@ PlayerFrame::file_add_ov()
}
if (r == wxID_OK) {
- DCPOMATIC_ASSERT(_film);
- DCPOMATIC_ASSERT(!_film->content().empty());
- auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front());
+ DCPOMATIC_ASSERT(!_playlist.empty());
+ auto dcp = std::dynamic_pointer_cast<DCPContent>(_playlist.front().first);
DCPOMATIC_ASSERT(dcp);
try {
@@ -774,7 +751,7 @@ PlayerFrame::file_add_ov()
}
auto job = make_shared<ExamineContentJob>(vector<shared_ptr<Content>>{dcp}, true);
- _examine_job_connection = job->Finished.connect(boost::bind(&PlayerFrame::prepare_to_play_film, this, Config::instance()->player_crop_output_ratio()));
+ _examine_job_connection = job->Finished.connect(boost::bind(&PlayerFrame::take_playlist_entry, this));
JobManager::instance()->add(job);
display_progress(variant::wx::dcpomatic_player(), _("Loading content"));
@@ -789,21 +766,19 @@ PlayerFrame::file_add_kdm()
FileDialog dialog(this, _("Select KDM"), char_to_wx("XML files|*.xml|All files|*.*"), wxFD_MULTIPLE, "AddKDMPath");
if (dialog.show()) {
- DCPOMATIC_ASSERT(_film);
- auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front());
+ DCPOMATIC_ASSERT(!_playlist.empty());
+ auto dcp = std::dynamic_pointer_cast<DCPContent>(_playlist.front().first);
DCPOMATIC_ASSERT(dcp);
try {
- if (dcp) {
- dcp::ScopeGuard sg([this]() {
- _viewer.set_coalesce_player_changes(false);
- });
- _viewer.set_coalesce_player_changes(true);
- for (auto path: dialog.paths()) {
- dcp->add_kdm(dcp::EncryptedKDM(dcp::file_to_string(path)));
- _kdms.push_back(path);
- }
- examine_content();
+ dcp::ScopeGuard sg([this]() {
+ _viewer.set_coalesce_player_changes(false);
+ });
+ _viewer.set_coalesce_player_changes(true);
+ for (auto path: dialog.paths()) {
+ dcp->add_kdm(dcp::EncryptedKDM(dcp::file_to_string(path)));
+ _kdms.push_back(path);
}
+ examine_content();
} catch (exception& e) {
error_dialog(this, wxString::Format(_("Could not load KDM.")), std_to_wx(e.what()));
return;
@@ -871,7 +846,11 @@ PlayerFrame::file_history(wxCommandEvent& event)
void
PlayerFrame::file_close()
{
- reset_film();
+ _playlist.clear();
+ _playlist_position = 0;
+ _controls->playlist_changed();
+
+ take_playlist_entry();
_info->triggered_update();
set_menu_sensitivity();
}
@@ -901,7 +880,8 @@ PlayerFrame::edit_preferences()
void
PlayerFrame::view_cpl(wxCommandEvent& ev)
{
- auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front());
+ DCPOMATIC_ASSERT(!_playlist.empty());
+ auto dcp = std::dynamic_pointer_cast<DCPContent>(_playlist.front().first);
DCPOMATIC_ASSERT(dcp);
auto cpls = dcp->cpls();
int id = ev.GetId() - ID_view_cpl;
@@ -1041,8 +1021,8 @@ PlayerFrame::view_closed_captions()
void
PlayerFrame::tools_verify()
{
- DCPOMATIC_ASSERT(!_film->content().empty());
- auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front());
+ DCPOMATIC_ASSERT(!_playlist.empty());
+ auto dcp = std::dynamic_pointer_cast<DCPContent>(_playlist.front().first);
DCPOMATIC_ASSERT(dcp);
VerifyDCPDialog dialog(this, _("Verify DCP"), dcp->directories(), _kdms);
@@ -1053,8 +1033,8 @@ PlayerFrame::tools_verify()
void
PlayerFrame::tools_audio_graph()
{
- DCPOMATIC_ASSERT(!_film->content().empty());
- auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front());
+ DCPOMATIC_ASSERT(!_playlist.empty());
+ auto dcp = std::dynamic_pointer_cast<DCPContent>(_playlist.front().first);
DCPOMATIC_ASSERT(dcp);
_audio_dialog.reset(this, _film, dcp);
@@ -1265,7 +1245,7 @@ PlayerFrame::update_from_config(Config::Property prop)
void
PlayerFrame::set_menu_sensitivity()
{
- auto const have_content = _film && !_film->content().empty();
+ auto const have_content = !_playlist.empty();
auto const dcp = _viewer.dcp();
auto const playable = dcp && !dcp->needs_assets() && !dcp->needs_kdm();
_tools_verify->Enable(have_content);
@@ -1322,3 +1302,276 @@ PlayerFrame::go_to_end()
{
_viewer.seek(_film->length() - _viewer.one_video_frame(), true);
}
+
+static
+optional<dcp::EncryptedKDM>
+get_kdm_from_directory(shared_ptr<DCPContent> dcp)
+{
+ using namespace boost::filesystem;
+ auto kdm_dir = Config::instance()->player_kdm_directory();
+ if (!kdm_dir) {
+ return {};
+ }
+ for (auto i: directory_iterator(*kdm_dir)) {
+ try {
+ if (file_size(i.path()) < MAX_KDM_SIZE) {
+ dcp::EncryptedKDM kdm(dcp::file_to_string(i.path()));
+ if (kdm.cpl_id() == dcp->cpl()) {
+ return kdm;
+ }
+ }
+ } catch (std::exception& e) {
+ /* Hey well */
+ }
+ }
+
+ return {};
+}
+
+
+bool
+PlayerFrame::set_playlist(vector<ShowPlaylistEntry> playlist)
+{
+ bool was_playing = false;
+ if (_viewer.playing()) {
+ was_playing = true;
+ _viewer.stop();
+ }
+
+ wxProgressDialog dialog(variant::wx::dcpomatic(), _("Loading playlist and KDMs"));
+
+ _playlist.clear();
+ _playlist_position = 0;
+
+ auto const store = ShowPlaylistContentStore::instance();
+ for (auto const& entry: playlist) {
+ dialog.Pulse();
+ auto content = store->get(entry);
+ if (!content) {
+ error_dialog(this, _("This playlist cannot be loaded as some content is missing."));
+ _playlist.clear();
+ _controls->playlist_changed();
+ return false;
+ }
+
+ auto dcp = dynamic_pointer_cast<DCPContent>(content);
+ if (dcp && dcp->needs_kdm()) {
+ optional<dcp::EncryptedKDM> kdm;
+ kdm = get_kdm_from_directory(dcp);
+ if (kdm) {
+ try {
+ dcp->add_kdm(*kdm);
+ dcp->examine(shared_ptr<Job>(), true);
+ } catch (KDMError& e) {
+ error_dialog(this, _("Could not load KDM."));
+ }
+ }
+ if (dcp->needs_kdm()) {
+ /* We didn't get a KDM for this */
+ error_dialog(this, _("This playlist cannot be loaded as a KDM is missing or incorrect."));
+ _playlist.clear();
+ _controls->playlist_changed();
+ return false;
+ }
+ }
+ _playlist.push_back({content, entry.crop_to_ratio()});
+ }
+
+ take_playlist_entry();
+
+ if (was_playing) {
+ _viewer.start();
+ }
+
+ _controls->playlist_changed();
+
+ return true;
+}
+
+
+/** Stop the viewer, take the thing at _playlist_position and prepare to play it.
+ * Set up to play nothing if the playlist is empty, or we're off the
+ * end of it.
+ * @return true if the viewer was playing when this method was called.
+ */
+bool
+PlayerFrame::take_playlist_entry()
+{
+ boost::optional<float> crop_to_ratio;
+
+ if (_playlist_position < 0 || _playlist_position >= static_cast<int>(_playlist.size())) {
+ _film = std::make_shared<Film>(boost::none);
+ } else {
+ auto const entry = _playlist[_playlist_position];
+
+ _film = std::make_shared<Film>(optional<boost::filesystem::path>());
+ _film->add_content({entry.first});
+
+ if (!entry.second) {
+ crop_to_ratio = Config::instance()->player_crop_output_ratio();
+ }
+ }
+
+ bool const playing = _viewer.playing();
+ if (playing) {
+ _viewer.stop();
+ }
+
+ /* Start off as Flat */
+ auto auto_ratio = Ratio::from_id("185");
+
+ _film->set_audio_channels(MAX_DCP_AUDIO_CHANNELS);
+
+ for (auto i: _film->content()) {
+ auto dcp = dynamic_pointer_cast<DCPContent>(i);
+
+ copy_dcp_markers_to_film(dcp, _film);
+
+ for (auto j: i->text) {
+ j->set_use(true);
+ }
+
+ if (i->video && i->video->size()) {
+ auto const r = Ratio::nearest_from_ratio(i->video->size()->ratio());
+ if (r.id() == "239") {
+ /* Any scope content means we use scope */
+ auto_ratio = r;
+ }
+ }
+
+ /* Any 3D content means we use 3D mode */
+ if (i->video && i->video->frame_type() != VideoFrameType::TWO_D) {
+ _film->set_three_d(true);
+ }
+
+ if (dcp->video_frame_rate()) {
+ _film->set_video_frame_rate(dcp->video_frame_rate().get());
+ }
+
+ switch (dcp->video_encoding().get_value_or(VideoEncoding::JPEG2000)) {
+ case VideoEncoding::JPEG2000:
+ _viewer.set_optimisation(Optimisation::JPEG2000);
+ break;
+ case VideoEncoding::MPEG2:
+ _viewer.set_optimisation(Optimisation::MPEG2);
+ break;
+ case VideoEncoding::COUNT:
+ DCPOMATIC_ASSERT(false);
+ }
+ }
+
+ set_audio_delay_from_config();
+
+ auto old = _cpl_menu->GetMenuItems();
+ for (auto const& i: old) {
+ _cpl_menu->Remove(i);
+ }
+
+ if (_film->content().size() == 1) {
+ /* Offer a CPL menu */
+ if (auto first = dynamic_pointer_cast<DCPContent>(_film->content().front())) {
+ int id = ID_view_cpl;
+ for (auto i: dcp::find_and_resolve_cpls(first->directories(), true)) {
+ auto j = _cpl_menu->AppendRadioItem(
+ id,
+ wxString::Format(char_to_wx("%s (%s)"), std_to_wx(i->content_title_text()).data(), std_to_wx(i->id()).data())
+ );
+ j->Check(!first->cpl() || i->id() == *first->cpl());
+ ++id;
+ }
+ }
+
+ if (crop_to_ratio) {
+ auto size = _film->content()[0]->video->size().get_value_or({1998, 1080});
+ int pixels = 0;
+ if (*crop_to_ratio > (2048.0 / 1080.0)) {
+ pixels = (size.height - (size.width / *crop_to_ratio)) / 2;
+ _film->content()[0]->video->set_crop(Crop{0, 0, std::max(0, pixels), std::max(0, pixels)});
+ } else {
+ pixels = (size.width - (size.height * *crop_to_ratio)) / 2;
+ _film->content()[0]->video->set_crop(Crop{std::max(0, pixels), std::max(0, pixels), 0, 0});
+ }
+ }
+ }
+
+ if (crop_to_ratio) {
+ _film->set_container(Ratio(*crop_to_ratio, "custom", "custom", {}, "custom"));
+ } else {
+ _film->set_container(auto_ratio);
+ }
+
+ _viewer.set_film(_film);
+ _viewer.seek(DCPTime(), true);
+ _viewer.set_eyes(_view_eye_left->IsChecked() ? Eyes::LEFT : Eyes::RIGHT);
+ _info->triggered_update();
+ set_menu_sensitivity();
+
+ _controls->set_film(_film);
+ return playing;
+}
+
+
+void
+PlayerFrame::viewer_finished()
+{
+ _playlist_position++;
+
+ /* Either get the next piece of content, or go black */
+ take_playlist_entry();
+
+ if (_playlist_position < static_cast<int>(_playlist.size())) {
+ /* Start the next piece of content */
+ _viewer.start();
+ } else {
+ /* Be ready to start again from the top of the playlist */
+ _playlist_position = 0;
+ }
+}
+
+
+
+bool
+PlayerFrame::can_do_next() const
+{
+ return _playlist_position < (static_cast<int>(_playlist.size()) - 1);
+}
+
+
+void
+PlayerFrame::next()
+{
+ _playlist_position++;
+ if (take_playlist_entry()) {
+ _viewer.start();
+ }
+}
+
+
+bool
+PlayerFrame::can_do_previous() const
+{
+ return _playlist_position > 0;
+}
+
+
+void
+PlayerFrame::previous()
+{
+ _playlist_position--;
+ if (take_playlist_entry()) {
+ _viewer.start();
+ }
+}
+
+
+vector<shared_ptr<Content>>
+PlayerFrame::playlist() const
+{
+ vector<shared_ptr<Content>> content;
+ for (auto entry: _playlist) {
+ content.push_back(entry.first);
+ }
+ return content;
+}
+
+
diff --git a/src/wx/player_frame.h b/src/wx/player_frame.h
index 84da6b3a6..e50bc7995 100644
--- a/src/wx/player_frame.h
+++ b/src/wx/player_frame.h
@@ -26,6 +26,7 @@
#include "wx_ptr.h"
#include "lib/config.h"
#include "lib/http_server.h"
+#include "lib/show_playlist_entry.h"
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
#include <wx/dnd.h>
@@ -65,14 +66,23 @@ public:
void too_many_frames_dropped();
void set_decode_reduction(boost::optional<int> reduction);
void load_dcp(boost::filesystem::path dir);
- void reset_film(std::shared_ptr<Film> film = std::make_shared<Film>(boost::none), boost::optional<float> crop_to_ratio = {});
- /* _film is now something new: set up to play it */
- void prepare_to_play_film(boost::optional<float> crop_to_ratio);
void set_audio_delay_from_config();
void load_stress_script(boost::filesystem::path path);
void idle();
+ /** Set the playlist. If we're currently playing, this will stop whatever is
+ * happening now and start playing this playlist.
+ */
+ bool set_playlist(std::vector<ShowPlaylistEntry> playlist);
+
+ std::vector<std::shared_ptr<Content>> playlist() const;
+
+ bool can_do_next() const;
+ void next();
+ bool can_do_previous() const;
+ void previous();
+
private:
void examine_content();
bool report_errors_from_last_job(wxWindow* parent) const;
@@ -115,6 +125,10 @@ private:
void go_to_start();
void go_to_end();
+ bool take_playlist_entry();
+ void prepare_to_play_film(boost::optional<float> crop_to_ratio);
+ void viewer_finished();
+
wxFrame* _dual_screen = nullptr;
bool _update_news_requested = false;
PlayerInformation* _info = nullptr;
@@ -134,6 +148,8 @@ private:
Controls* _controls;
wx_ptr<SystemInformationDialog> _system_information_dialog;
std::shared_ptr<Film> _film;
+ std::vector<std::pair<std::shared_ptr<Content>, boost::optional<float>>> _playlist;
+ int _playlist_position = 0;
boost::signals2::scoped_connection _config_changed_connection;
boost::signals2::scoped_connection _examine_job_connection;
wxMenuItem* _file_add_ov = nullptr;
diff --git a/src/wx/playlist_controls.cc b/src/wx/playlist_controls.cc
index d0d0c1d23..a0f91b00f 100644
--- a/src/wx/playlist_controls.cc
+++ b/src/wx/playlist_controls.cc
@@ -52,7 +52,11 @@ using std::shared_ptr;
using std::sort;
using std::string;
using std::vector;
+using std::weak_ptr;
using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
using namespace dcpomatic;
using namespace dcpomatic::ui;
@@ -72,26 +76,26 @@ PlaylistControls::PlaylistControls(wxWindow* parent, PlayerFrame* player, FilmVi
_button_sizer->Add(_stop_button, 0, wxEXPAND);
_button_sizer->Add(_next_button, 0, wxEXPAND);
- _spl_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER);
- _spl_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 740);
+ _playlists_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER);
+ _playlists_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 740);
auto left_sizer = new wxBoxSizer(wxVERTICAL);
- auto e_sizer = new wxBoxSizer(wxHORIZONTAL);
+ auto h_sizer = new wxBoxSizer(wxHORIZONTAL);
wxFont subheading_font(*wxNORMAL_FONT);
subheading_font.SetWeight(wxFONTWEIGHT_BOLD);
- auto spl_header = new wxBoxSizer(wxHORIZONTAL);
+ auto playlists_header = new wxBoxSizer(wxHORIZONTAL);
{
auto m = new StaticText(this, _("Playlists"));
m->SetFont(subheading_font);
- spl_header->Add(m, 1, wxALIGN_CENTER_VERTICAL);
+ playlists_header->Add(m, 1, wxALIGN_CENTER_VERTICAL);
}
- _refresh_spl_view = new Button(this, _("Refresh"));
- spl_header->Add(_refresh_spl_view, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP / 2);
+ _refresh_playlists_view = new Button(this, _("Refresh"));
+ playlists_header->Add(_refresh_playlists_view, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP / 2);
- left_sizer->Add(spl_header, 0, wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP);
- left_sizer->Add(_spl_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP);
+ left_sizer->Add(playlists_header, 0, wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP);
+ left_sizer->Add(_playlists_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP);
_content_view = new ContentView(this);
@@ -107,24 +111,54 @@ PlaylistControls::PlaylistControls(wxWindow* parent, PlayerFrame* player, FilmVi
left_sizer->Add(content_header, 0, wxTOP | wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP);
left_sizer->Add(_content_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP);
- _current_spl_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER);
- _current_spl_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 500);
- _current_spl_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 80);
- e_sizer->Add(left_sizer, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
- e_sizer->Add(_current_spl_view, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
+ auto right_sizer = new wxBoxSizer(wxVERTICAL);
- _v_sizer->Add(e_sizer, 1, wxEXPAND);
+ _next_playlist_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER);
+ _next_playlist_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 600);
+ _next_playlist_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 80);
+
+ auto next_playlist_header = new wxBoxSizer(wxHORIZONTAL);
+ {
+ auto m = new StaticText(this, _("Next playlist"));
+ m->SetFont(subheading_font);
+ next_playlist_header->Add(m, 1, wxALIGN_CENTER_VERTICAL);
+ }
+ _clear_next_playlist = new wxButton(this, wxID_ANY, _("Clear"));
+ next_playlist_header->Add(_clear_next_playlist, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP / 2);
+
+ right_sizer->Add(next_playlist_header, 0, wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP);
+ right_sizer->Add(_next_playlist_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP);
+
+ _current_playlist_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER);
+ _current_playlist_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 600);
+ _current_playlist_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 80);
+
+ auto current_playlist_header = new wxBoxSizer(wxHORIZONTAL);
+ {
+ auto m = new StaticText(this, _("Current playlist"));
+ m->SetFont(subheading_font);
+ current_playlist_header->Add(m, 1, wxALIGN_CENTER_VERTICAL);
+ }
+
+ right_sizer->Add(current_playlist_header, 0, wxTOP | wxBOTTOM | wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP);
+ right_sizer->Add(_current_playlist_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP);
+
+ h_sizer->Add(left_sizer, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
+ h_sizer->Add(right_sizer, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
+ _v_sizer->Add(h_sizer, 1, wxEXPAND);
_play_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::play_clicked, this));
_pause_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::pause_clicked, this));
_stop_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::stop_clicked, this));
_next_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::next_clicked, this));
_previous_button->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::previous_clicked, this));
- _spl_view->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind(&PlaylistControls::spl_selection_changed, this));
- _spl_view->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&PlaylistControls::spl_selection_changed, this));
- _viewer.Finished.connect(boost::bind(&PlaylistControls::viewer_finished, this));
- _refresh_spl_view->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::update_playlists, this));
+ _playlists_view->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind(&PlaylistControls::playlist_selection_changed, this));
+ _playlists_view->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&PlaylistControls::playlist_selection_changed, this));
+ _refresh_playlists_view->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::update_playlists, this));
_refresh_content_view->Bind(wxEVT_BUTTON, boost::bind(&ContentView::update, _content_view));
+ _clear_next_playlist->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::clear_next_playlist, this));
+
+ _content_view->Activated.connect(boost::bind(&PlaylistControls::content_activated, this, _1));
update_playlists();
_content_view->update();
@@ -132,40 +166,45 @@ PlaylistControls::PlaylistControls(wxWindow* parent, PlayerFrame* player, FilmVi
void
-PlaylistControls::started()
+PlaylistControls::clear_next_playlist()
{
- Controls::started();
- _play_button->Enable(false);
- _pause_button->Enable(true);
+ _next_playlist_view->DeleteAllItems();
+ _next_playlist.clear();
}
-/** Called when the viewer finishes a single piece of content, or it is explicitly stopped */
void
-PlaylistControls::stopped()
+PlaylistControls::content_activated(weak_ptr<Content> weak_content)
{
- Controls::stopped();
- _play_button->Enable(true);
- _pause_button->Enable(false);
+ if (auto content = weak_content.lock()) {
+ add_next_playlist_entry(ShowPlaylistEntry(content, {}));
+ }
}
void
-PlaylistControls::deselect_playlist()
+PlaylistControls::started()
{
- long int const selected = _spl_view->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
- if (selected != -1) {
- _selected_playlist = boost::none;
- _spl_view->SetItemState(selected, 0, wxLIST_STATE_SELECTED);
- }
- _player->reset_film();
+ Controls::started();
+ setup_sensitivity();
+}
+
+
+/** Called when the viewer finishes a single piece of content, or it is explicitly stopped */
+void
+PlaylistControls::stopped()
+{
+ Controls::stopped();
+ setup_sensitivity();
}
void
PlaylistControls::play_clicked()
{
- _viewer.start();
+ if (_player->set_playlist(_next_playlist)) {
+ _viewer.start();
+ }
}
@@ -173,92 +212,70 @@ void
PlaylistControls::setup_sensitivity()
{
Controls::setup_sensitivity();
- bool const active_job = _active_job && *_active_job != "examine_content";
- bool const c = _film && !_film->content().empty() && !active_job;
- _play_button->Enable(c && !_viewer.playing());
- _pause_button->Enable(_viewer.playing());
- _spl_view->Enable(!_viewer.playing());
- _next_button->Enable(can_do_next());
- _previous_button->Enable(can_do_previous());
+ _play_button->Enable(!_viewer.playing() && !_paused && !_next_playlist.empty());
+ _pause_button->Enable(_viewer.playing() || _paused);
+ _stop_button->Enable(_viewer.playing() || _paused);
+ _next_button->Enable(_player->can_do_next());
+ _previous_button->Enable(_player->can_do_previous());
}
void
PlaylistControls::pause_clicked()
{
- _viewer.stop();
+ if (_paused) {
+ _viewer.start();
+ _paused = false;
+ } else {
+ _viewer.stop();
+ _paused = true;
+ }
+ setup_sensitivity();
}
void
PlaylistControls::stop_clicked()
{
+ _paused = false;
_viewer.stop();
_viewer.seek(DCPTime(), true);
- if (_selected_playlist) {
- _selected_playlist_position = 0;
- update_current_content();
- }
- deselect_playlist();
-}
-
-
-bool
-PlaylistControls::can_do_previous()
-{
- return _selected_playlist && (_selected_playlist_position - 1) >= 0;
+ _player->set_playlist({});
}
void
PlaylistControls::previous_clicked()
{
- if (!can_do_previous()) {
- return;
- }
-
- _selected_playlist_position--;
- update_current_content();
-}
-
-
-bool
-PlaylistControls::can_do_next()
-{
- return _selected_playlist && (_selected_playlist_position + 1) < static_cast<int>(_playlists->entries(*_selected_playlist).size());
+ _player->previous();
}
void
PlaylistControls::next_clicked()
{
- if (!can_do_next()) {
- return;
- }
-
- _selected_playlist_position++;
- update_current_content();
+ _player->next();
}
void
-PlaylistControls::add_playlist_to_list(ShowPlaylist spl)
+PlaylistControls::add_playlist_to_list(ShowPlaylist playlist)
{
- int const N = _spl_view->GetItemCount();
+ int const N = _playlists_view->GetItemCount();
wxListItem it;
it.SetId(N);
it.SetColumn(0);
- auto id = _playlists->get_show_playlist_id(spl.uuid());
+ auto id = _playlists->get_show_playlist_id(playlist.uuid());
DCPOMATIC_ASSERT(id);
it.SetData(id->get());
- string t = spl.name();
+ string t = playlist.name();
- if (_playlists->missing(spl.uuid())) {
+ if (_playlists->missing(playlist.uuid())) {
t += " (content missing)";
}
it.SetText(std_to_wx(t));
- _spl_view->InsertItem(it);
+ _playlists_view->InsertItem(it);
}
@@ -267,14 +284,12 @@ PlaylistControls::update_playlists()
{
using namespace boost::filesystem;
- _spl_view->DeleteAllItems();
+ _playlists_view->DeleteAllItems();
_playlists.reset(new ShowPlaylistList());
for (auto i: _playlists->show_playlists()) {
add_playlist_to_list(i.second);
}
-
- _selected_playlist = boost::none;
}
@@ -304,20 +319,31 @@ PlaylistControls::get_kdm_from_directory(shared_ptr<DCPContent> dcp)
void
-PlaylistControls::spl_selection_changed()
+PlaylistControls::add_next_playlist_entry(ShowPlaylistEntry entry)
{
- long int selected = _spl_view->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+ wxListItem it;
+ it.SetId(_next_playlist_view->GetItemCount());
+ it.SetColumn(0);
+ it.SetText(std_to_wx(entry.name()));
+ _next_playlist_view->InsertItem(it);
+ _next_playlist.push_back(entry);
+
+ setup_sensitivity();
+}
+
+
+void
+PlaylistControls::playlist_selection_changed()
+{
+ long int selected = _playlists_view->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
if (selected == -1) {
- _current_spl_view->DeleteAllItems();
- _selected_playlist = boost::none;
return;
}
- auto const id = ShowPlaylistID(_spl_view->GetItemData(selected));
+ auto const id = ShowPlaylistID(_playlists_view->GetItemData(selected));
if (_playlists->missing(id)) {
error_dialog(this, _("This playlist cannot be loaded as some content is missing."));
- deselect_playlist();
return;
}
@@ -326,72 +352,11 @@ PlaylistControls::spl_selection_changed()
return;
}
- select_playlist(id, 0);
-}
-
-
-void
-PlaylistControls::select_playlist(ShowPlaylistID selected, int position)
-{
- wxProgressDialog dialog(variant::wx::dcpomatic(), _("Loading playlist and KDMs"));
-
- auto const store = ShowPlaylistContentStore::instance();
- for (auto const& i: _playlists->entries(selected)) {
- dialog.Pulse();
- auto dcp = dynamic_pointer_cast<DCPContent>(store->get(i));
- if (dcp && dcp->needs_kdm()) {
- optional<dcp::EncryptedKDM> kdm;
- kdm = get_kdm_from_directory(dcp);
- if (kdm) {
- try {
- dcp->add_kdm(*kdm);
- dcp->examine(shared_ptr<Job>(), true);
- } catch (KDMError& e) {
- error_dialog(this, _("Could not load KDM."));
- }
- }
- if (dcp->needs_kdm()) {
- /* We didn't get a KDM for this */
- error_dialog(this, _("This playlist cannot be loaded as a KDM is missing or incorrect."));
- deselect_playlist();
- return;
- }
- }
- }
-
- _current_spl_view->DeleteAllItems();
-
- int N = 0;
- for (auto const& i: _playlists->entries(selected)) {
- wxListItem it;
- it.SetId(N);
- it.SetColumn(0);
- it.SetText(std_to_wx(i.name()));
- _current_spl_view->InsertItem(it);
- ++N;
+ _next_playlist_view->DeleteAllItems();
+ _next_playlist.clear();
+ for (auto const& i: _playlists->entries(id)) {
+ add_next_playlist_entry(i);
}
-
- _selected_playlist = selected;
- _selected_playlist_position = position;
- dialog.Pulse();
- reset_film();
- dialog.Pulse();
- update_current_content();
-}
-
-
-void
-PlaylistControls::reset_film()
-{
- DCPOMATIC_ASSERT(_selected_playlist);
-
- auto film = std::make_shared<Film>(optional<boost::filesystem::path>());
-
- auto const entries = _playlists->entries(*_selected_playlist);
- DCPOMATIC_ASSERT(_selected_playlist_position < static_cast<int>(entries.size()));
- auto const entry = entries[_selected_playlist_position];
- film->add_content(vector<shared_ptr<Content>>{ShowPlaylistContentStore::instance()->get(entry)});
- _player->reset_film(film, entry.crop_to_ratio());
}
@@ -409,50 +374,18 @@ PlaylistControls::config_changed(int property)
void
-PlaylistControls::update_current_content()
-{
- DCPOMATIC_ASSERT(_selected_playlist);
-
- wxProgressDialog dialog(variant::wx::dcpomatic(), _("Loading content"));
-
- setup_sensitivity();
- dialog.Pulse();
- reset_film();
-}
-
-
-/** One piece of content in our SPL has finished playing */
-void
-PlaylistControls::viewer_finished()
+PlaylistControls::playlist_changed()
{
- if (!_selected_playlist) {
- return;
- }
+ _current_playlist_view->DeleteAllItems();
- _selected_playlist_position++;
- if (_selected_playlist_position < int(_playlists->entries(*_selected_playlist).size())) {
- /* Next piece of content on the SPL */
- update_current_content();
- _viewer.start();
- } else {
- /* Finished the whole SPL */
- _selected_playlist_position = 0;
- _player->reset_film();
- _play_button->Enable(true);
- _pause_button->Enable(false);
+ int N = 0;
+ for (auto content: _player->playlist()) {
+ wxListItem it;
+ it.SetId(N++);
+ it.SetColumn(0);
+ ShowPlaylistEntry entry(content, {});
+ it.SetText(std_to_wx(entry.name()));
+ _current_playlist_view->InsertItem(it);
}
}
-
-void
-PlaylistControls::play()
-{
- play_clicked();
-}
-
-
-void
-PlaylistControls::stop()
-{
- stop_clicked();
-}
diff --git a/src/wx/playlist_controls.h b/src/wx/playlist_controls.h
index 71f9d7f47..da17358cb 100644
--- a/src/wx/playlist_controls.h
+++ b/src/wx/playlist_controls.h
@@ -41,8 +41,7 @@ class PlaylistControls : public Controls
public:
PlaylistControls(wxWindow* parent, dcpomatic::ui::PlayerFrame* player, FilmViewer& viewer);
- void play() override;
- void stop() override;
+ void playlist_changed() override;
private:
void play_clicked();
@@ -50,21 +49,20 @@ private:
void stop_clicked();
void next_clicked();
void previous_clicked();
- void add_playlist_to_list(ShowPlaylist spl);
- void update_content_directory();
+
void update_playlists();
- void spl_selection_changed();
- void select_playlist(ShowPlaylistID selected, int position);
+ void playlist_selection_changed();
+
+ void config_changed(int) override;
void started() override;
void stopped() override;
+
void setup_sensitivity() override;
- void config_changed(int) override;
- void viewer_finished();
- void reset_film();
- void update_current_content();
- bool can_do_previous();
- bool can_do_next();
- void deselect_playlist();
+
+ void add_playlist_to_list(ShowPlaylist spl);
+ void add_next_playlist_entry(ShowPlaylistEntry entry);
+ void clear_next_playlist();
+ void content_activated(std::weak_ptr<Content> content);
boost::optional<dcp::EncryptedKDM> get_kdm_from_directory(std::shared_ptr<DCPContent> dcp);
@@ -78,11 +76,15 @@ private:
ContentView* _content_view;
wxButton* _refresh_content_view;
- wxListCtrl* _spl_view;
- wxButton* _refresh_spl_view;
- wxListCtrl* _current_spl_view;
+ wxListCtrl* _playlists_view;
+ wxButton* _refresh_playlists_view;
+ wxListCtrl* _next_playlist_view;
+ wxButton* _clear_next_playlist;
+
+ wxListCtrl* _current_playlist_view;
std::unique_ptr<ShowPlaylistList> _playlists;
- boost::optional<ShowPlaylistID> _selected_playlist;
- int _selected_playlist_position;
+ std::vector<ShowPlaylistEntry> _next_playlist;
+
+ bool _paused = false;
};
diff --git a/src/wx/standard_controls.cc b/src/wx/standard_controls.cc
index 9126f24b4..03934e482 100644
--- a/src/wx/standard_controls.cc
+++ b/src/wx/standard_controls.cc
@@ -87,18 +87,3 @@ StandardControls::setup_sensitivity ()
_play_button->Enable (_film && !_film->content().empty() && !active_job);
}
-
-void
-StandardControls::play ()
-{
- _play_button->SetValue (true);
- play_clicked ();
-}
-
-
-void
-StandardControls::stop ()
-{
- _play_button->SetValue (false);
- play_clicked ();
-}
diff --git a/src/wx/standard_controls.h b/src/wx/standard_controls.h
index 1b8618763..21f1b0039 100644
--- a/src/wx/standard_controls.h
+++ b/src/wx/standard_controls.h
@@ -27,9 +27,6 @@ class StandardControls : public Controls
public:
StandardControls(wxWindow* parent, FilmViewer& viewer, bool editor_controls);
- void play () override;
- void stop () override;
-
private:
void check_play_state ();
void play_clicked ();
diff --git a/web/index.html b/web/index.html
index db313c48e..b0b5b4505 100644
--- a/web/index.html
+++ b/web/index.html
@@ -24,33 +24,6 @@ function stop() {
fetch("/api/v1/stop", { method: "POST" });
}
-function selectedPlaylistId()
-{
- var children = document.getElementById("content-and-playlists-inner").children;
- for (var i = 0; i < children.length; i++) {
- if (children[i].classList.contains("selected")) {
- return children[i].uuid;
- }
- }
-
- return null;
-}
-
-
-function setSelectedPlaylist(uuid)
-{
- var children = document.getElementById("content-and-playlists-inner").children;
- for (var i = 0; i < children.length; i++) {
- var child = children[i];
- if (child.uuid == uuid) {
- child.classList.add("selected");
- } else {
- child.classList.remove("selected");
- }
- }
-};
-
-
// Fetch all playlists and put them in the shared content/playlists div
function showPlaylists()
{
@@ -67,7 +40,20 @@ function showPlaylists()
li.appendChild(document.createTextNode(playlist.name));
li.uuid = playlist['uuid'];
li.onclick = function() {
- setSelectedPlaylist(playlist['uuid']);
+ fetch("/api/v1/playlist/" + playlist['uuid']).then(response => {
+ response.json().then(data => {
+ var nextPlaylist = document.getElementById('next-playlist');
+ nextPlaylist.innerHTML = "";
+ var ul = document.createElement("ul");
+ nextPlaylist.appendChild(ul);
+ data.content.forEach(content => {
+ var li = document.createElement("li");
+ li.appendChild(document.createTextNode(content.name));
+ ul.appendChild(li);
+ });
+ console.log(data);
+ })
+ });
};
document.getElementById('content-and-playlists-inner').appendChild(li);
});
@@ -159,6 +145,8 @@ li.playlist.selected {
SIDEBAR
+<div style="margin-left: 15%;">
+
<div id="controls" style="float: left; width: 20%;">
<button name="play" value="play" onclick="play()">Play</button>
<button name="stop" value="stop" onclick="stop()">Stop</button>
@@ -169,14 +157,29 @@ SIDEBAR
</table>
</div>
-<div id="current-playlist" style="float: left; width: 20%;">
+<div style="float: right; width: 70%;">
+
+<div style="float: right; width: 100%;">
+Next playlist:
+<div id="next-playlist" style="width: 100%; height: 40vh;">
+</div>
+</div>
+
+<div style="float: right; width: 100%; margin-top: 4em;">
Current playlist:
+<div id="current-playlist" style="width: 100%; height: 10vh;">
+</div>
</div>
-<div id="content-and-playlists" style="float: left; width: 20%;">
+</div>
+
+<div id="content-and-playlists" style="float: left; width: 20%; left-margin: 10%;">
<div id="playlists-tab" onclick="showPlaylists();" class="tab selected">Playlists</div>
<div id="content-tab" onclick="showContent();" class="tab">Content</div>
<div id="content-and-playlists-inner" style="margin-top: 5pt; margin-bottom: 5pt;"></div>
</div>
+
+</div>
+
</body>
</html>
diff --git a/web/sidebar.html b/web/sidebar.html
index 7ea629cc0..b7c3e7555 100644
--- a/web/sidebar.html
+++ b/web/sidebar.html
@@ -1,7 +1,9 @@
<style>
div.nav {
- float: left;
margin-right: 4em;
+ height: 100vh;
+ width: 10%;
+ position: fixed;
}
li.nav {
border: 1px solid rgba(57, 61, 65, 0.50);