From e2f7b00557c4ea3c81d3c48c875213fb06c5a063 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Tue, 5 Jun 2018 21:53:42 -0700 Subject: [PATCH 01/15] Use Mojolicious instead of CGI and CGI::Ajax --- LICENSE | 2 +- Makefile | 9 +- README.md | 65 +--- index.pl | 328 ------------------ msc-gui | 229 ++++++++++++ msc-gui.service | 16 + {themes/default => public}/favicon.ico | Bin {themes/default => public}/stylesheet.css | 40 ++- .../default => public}/title-background.png | Bin {themes/default => public}/title-image.png | Bin templates/dashboard.html.ep | 7 + templates/disabled-world.html.ep | 3 + templates/enabled-world.html.ep | 29 ++ templates/layouts/default.html.ep | 18 + templates/worlds.html.ep | 6 + themes/default/disabled-world.xhtml | 3 - themes/default/index.xhtml | 17 - themes/default/world.xhtml | 25 -- 18 files changed, 352 insertions(+), 445 deletions(-) delete mode 100755 index.pl create mode 100644 msc-gui create mode 100644 msc-gui.service rename {themes/default => public}/favicon.ico (100%) rename {themes/default => public}/stylesheet.css (69%) rename {themes/default => public}/title-background.png (100%) rename {themes/default => public}/title-image.png (100%) create mode 100644 templates/dashboard.html.ep create mode 100644 templates/disabled-world.html.ep create mode 100644 templates/enabled-world.html.ep create mode 100644 templates/layouts/default.html.ep create mode 100644 templates/worlds.html.ep delete mode 100644 themes/default/disabled-world.xhtml delete mode 100644 themes/default/index.xhtml delete mode 100644 themes/default/world.xhtml diff --git a/LICENSE b/LICENSE index af9b867..f21c0d5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2016 Jason M. Wood +Copyright (c) 2018 Jason M. Wood All rights reserved. diff --git a/Makefile b/Makefile index c1d17d9..e5ddc0f 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,22 @@ MSC_USER := minecraft MSC_GROUP := minecraft MSC_GUI_HOME := /opt/mscs/gui +MSC_GUI_SERVICE := /etc/systemd/system/msc-gui.service .PHONY: install update clean install: $(MSC_GUI_HOME) update chown -R $(MSC_USER):$(MSC_GROUP) $(MSC_GUI_HOME) + systemctl -f enable msc-gui.service; update: - cp index.pl $(MSC_GUI_HOME) - cp -R themes $(MSC_GUI_HOME) + install -m 0755 msc-gui $(MSC_GUI_HOME) + install -m 0644 msc-gui.service $(MSC_GUI_SERVICE); + cp -R public $(MSC_GUI_HOME) + cp -R templates $(MSC_GUI_HOME) clean: + systemctl -f disable msc-gui.service; rm -R $(MSC_GUI_HOME) $(MSC_GUI_HOME): diff --git a/README.md b/README.md index cc6cc84..85701fa 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,12 @@ usable state, this message will be removed. ## Prerequisites for installation -The Minecraft Server Control GUI uses Perl to present a web-based interface to the +The Minecraft Server Control GUI uses Perl and Mojolicious, a Perl-based web +framework, to present a web-based interface to the [Minecraft Server Control Script](https://github.com/MinecraftServerControl/mscs). -As such, the `mscs` script must be [installed](https://github.com/MinecraftServerControl/mscs/blob/master/README.md#installation) -and working for the GUI to function. Since the GUI is web based, you will need -to have a web server installed and working. These directions assume you are -using [Apache](https://httpd.apache.org), but any web server solution should -function. To install Apache: - - sudo apt-get install apache2 +As such, the `mscs` script must be +[installed](https://github.com/MinecraftServerControl/mscs/blob/master/README.md#installation) +and working for the GUI to function. ## Installation @@ -56,58 +53,6 @@ by running: sudo make install - -#### Apache - -Here is an example of a file that can be placed in the -`/etc/apache2/sites-enabled/` directory to enable Apache to run a webserver -on port `80` on the host `minecraft.server.com` from the directory -`/var/www`. Change these values to suit your the needs of your website. This -configuration would make the GUI available at `http:\\localhost\gui`. - -``` - - ServerName minecraft.server.com - ServerAdmin webmaster@localhost - DocumentRoot /var/www - - Alias /gui /var/www/gui - - Order Allow,Deny - Allow from all - AllowOverride None - AddHandler cgi-script .pl - Options +ExecCGI -MultiViews -Indexes -Includes +FollowSymLinks - - - -``` - -Make sure to create a symbolic link so that Apache can actually find the GUI: - - sudo ln -s /opt/mscs/gui /var/www/gui - -You will also need to enable the Apache CGI module and restart Apache for the -GUI to work: - - sudo a2enmod cgi - sudo service apache2 restart - -#### Permissions - -To allow Apache and the MSC-GUI access to MSCS, use your favorite editor to -create a new file in the `/etc/sudoers.d` folder: - - sudo editor /etc/sudoers.d/mscs - -and add this text : - - # Allow www-data to execute the msctl command as the minecraft user. - www-data ALL=(minecraft:minecraft) NOPASSWD:SETENV: /usr/local/bin/msctl - -## Getting started guide - - ## License See [LICENSE](LICENSE) diff --git a/index.pl b/index.pl deleted file mode 100755 index 79ec7db..0000000 --- a/index.pl +++ /dev/null @@ -1,328 +0,0 @@ -#! /usr/bin/perl - -=head1 NAME - - MSC GUI. - -=head1 SYNOPSIS - - Creates a web-based GUI for MSC. - -=head1 DESCRIPTION - - This program creates a web-based Graphical User Interface (GUI) for - Minecraft Server Control (MSC). - -=head1 DEPENDENCIES - - This program depends on CGI and CGI::Ajax. - -=head1 FEEDBACK - -=head2 Mailing Lists - - No mailing list currently exists. - -=head2 Reporting Bugs - - Report bugs to the author directly. - -=head1 AUTHOR - Jason Wood - - Email sandain@hotmail.com - -=head1 COPYRIGHT AND LICENSE - - Copyright (c) 2016 Jason M. Wood - - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - -=head1 APPENDIX - - The rest of the documentation details each of the object methods. - -=cut - -use strict; -use warnings; - -use CGI qw/:standard/; -use CGI::Ajax; - -my $URI = ''; -my $THEME = 'themes/default'; - -my $TITLE = 'Minecraft Server Control'; - -my $CONTENT_TYPE = 'application/xhtml+xml'; -my $COMPATIBLE_CONTENT_TYPE = 'text/html'; - -my $CGI = new CGI; -my $AJAX = new CGI::Ajax (display_content => \&display_content); - -# Refresh the content every thirty seconds. -my $REFRESH_TIMER = 30 * 1000; - -# Create the menu. -my %menu = ( - 'dashboard' => { function => \&create_dashboard_content, order => 0 } -); - -# Figure out the best content-type to use. Use a backward-compatible -# content-type for certain user-agents. -my $content_type = $CONTENT_TYPE; -$content_type = $COMPATIBLE_CONTENT_TYPE if ( - defined $CGI->user_agent && - ($CGI->user_agent =~ /MSIE/ || $CGI->user_agent =~ /Lynx/) -); - -# Add the enabled worlds to the menu. -foreach my $line (mscs ("ls enabled")) { - if ($line =~ /^\s*(\w+):/) { - my $function = create_world_content ($1); - my $order = scalar keys %menu; - $menu{$1}{function} = $function; - $menu{$1}{order} = $order; - } -} - -# Add the disabled worlds to the menu. -foreach my $line (mscs ("ls disabled")) { - if ($line =~ /^\s*(\w+):/) { - my $function = create_disabled_world_content ($1); - my $order = scalar keys %menu; - $menu{$1}{function} = $function; - $menu{$1}{order} = $order; - } -} - -# Generate the tags. -my %tags = ( - 'title' => $TITLE, - 'uri' => $URI, - 'theme' => $THEME . "/", - 'title_image' => $URI . $THEME . "/title-image.png" -); - -# Add some preformated XHTML to the tags. -$tags{'xhtml_menu'} = create_menu_content (); -$tags{'xhtml_content'} = create_main_content (); -$tags{'xhtml_script'} = ""; - -# Load the index. -my $index = load_theme ($THEME . '/index.xhtml'); - -# Output the XHTML. -print $AJAX->build_html ( - $CGI, $index, { -charset => 'UTF-8', -type => $content_type } -); - - -=head2 create_dashboard_content - - Title : create_dashboard_content - Usage : - Function : Creates the dashboard content. - Returns : The content for the dashboard. - Args : - -=cut - -sub create_dashboard_content { - my $dashboard = "

Displaying Content: Dashboard

\n"; - return $dashboard; -} - -=head2 create_main_content - - Title : create_main_content - Usage : - Function : - Returns : XHTML to display the content. - Args : - -=cut - -sub create_main_content { - my $content = ""; - # Check if content needs to be displayed. - my %vars = $CGI->Vars; - if ( - defined $vars{display_content} && - defined $menu{$vars{display_content}} - ) { - $content .= &{$menu{$vars{display_content}}{function}}; - } - else { - # Otherwise default to the dashboard. - $content .= &{$menu{'dashboard'}{function}}; - } - return $content; -} - -=head2 create_menu_content - - Title : create_menu_content - Usage : - Function : - Returns : XHTML to display the menu. - Args : - -=cut - -sub create_menu_content { - my $menu = ""; - foreach my $item (sort {$menu{$a}{order} <=> $menu{$b}{order}} keys %menu) { - $menu .= "" . $item . "\n"; - } - return $menu; -} - -=head2 create_world_content - - Title : create_world_content - Usage : - Function : Creates the world content for a world of interest. - Returns : The content for the world of interest. - Args : world - The world of interest. - -=cut - -sub create_world_content { - my ($world) = @_; - return sub { - return load_theme ($THEME . '/world.xhtml', $world); - }; -} - -=head2 create_disabled_world_content - - Title : create_disabled_world_content - Usage : - Function : Creates the world content for a disabled world of interest. - Returns : The content for the disabled world of interest. - Args : world - The disabled world of interest. - -=cut - -sub create_disabled_world_content { - my ($world) = @_; - return sub { - return load_theme ($THEME . '/disabled-world.xhtml', $world); - }; -} - -=head2 display_content - - Title : display_content - Usage : - Function : Returns the content to be displayed. - Returns : The content of interest. - Args : item - The menu item for the content of interest. - -=cut - -sub display_content { - my ($item) = @_; - return &{$menu{$item}{function}}; -} - -=head2 load_theme - - Title : load_theme - Usage : - Function : Loads and renders the given theme file. - Returns : The rendered theme file. - Args : file - The theme file to load. - world - Optional world. - -=cut - -sub load_theme { - my ($file, $world) = @_; - my %world_tags = (); - if (defined $world) { - my $backups = mscs ("list-backups", $world); - my @query = split /\t/, mscs ("query", $world); - %world_tags = ( - "world_name" => $world, - "backup_list" => $backups, - "query_motd" => $query[6], - "query_gametype" => $query[8], - "query_gameid" => $query[10], - "query_version" => $query[12], - "query_plugins" => $query[14], - "query_map" => $query[16], - "query_numplayers" => $query[18], - "query_maxplayers" => $query[20], - "query_hostport" => $query[22], - "query_hostip" => $query[24], - "query_players" => $query[29], - ); - } - my $theme = ''; - open (THEME, $file) or - die "Can't open theme " . $file . ": $!"; - while (my $line = ) { - while ($line =~ /<\?msc_(\w*)\?>/) { - my $t = ''; - if (defined $tags{$1}) { - $t = $tags{$1}; - } - elsif (defined $world_tags{$1}) { - $t = $world_tags{$1}; - } - $line =~ s/(<\?msc_$1\?>)/$t/; - } - $theme .= $line; - } - close (THEME); - return $theme; -} - -=head2 mscs - - Title : mscs - Usage : - Function : Run the mscs command with the provided arguments. - Returns : - Args : args - The command line arguments for mscs. - -=cut - -sub mscs { - return `mscs @_`; -} diff --git a/msc-gui b/msc-gui new file mode 100644 index 0000000..1e988e2 --- /dev/null +++ b/msc-gui @@ -0,0 +1,229 @@ +#!/usr/bin/env perl + +=head1 NAME + + MSC-GUI + +=head1 SYNOPSIS + + +=head1 DESCRIPTION + + Minecraft Server Control GUI (MSC-GUI) is a new web-based interface to the + Minecraft Server Control Script that has been controlling many Linux and + UNIX powered Minecraft servers since it was first released in 2011. + +=head1 DEPENDENCIES + + MSC-GUI requires Perl version 5.10.1 or greater, in addition to Mojolicious. + +=head1 FEEDBACK + +=head2 Reporting Bugs + + Report bugs to the GitHub issue tracker at: + https://github.com/MinecraftServerControl/msc-gui/issues + +=head1 AUTHOR - Jason Wood + + Email sandain-at-hotmail.com + +=head1 COPYRIGHT AND LICENSE + + Copyright (c) 2018 Jason M. Wood + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +=head1 APPENDIX + + The rest of the documentation details each of the object methods. + +=cut + +use strict; +use warnings; +use utf8; +use feature ':5.10'; + +use Mojolicious::Lite; +use Mojo::ByteStream; + +=head2 Helper Functions + +=head3 msc_enabled_world_list + + Returns an unordered list in HTML format containing enabled worlds. + +=cut + +helper 'msc_enabled_world_list' => sub { + my $self = shift; + my $result = join "\n", map { + "
  • $_
  • " + } &getEnabledWorlds; + return new Mojo::ByteStream ("
      \n$result\n
    "); +}; + +=head3 msc_disabled_world_list + + Returns an unordered list in HTML format containing disabled worlds. + +=cut + +helper 'msc_disabled_world_list' => sub { + my $self = shift; + my $result = join "\n", map { + "
  • $_
  • " + } &getDisabledWorlds; + return new Mojo::ByteStream ("
      \n$result\n
    "); +}; + +=head3 msc_menu + + Returns the menu in HTML format. + +=cut + +helper 'msc_menu' => sub { + my $self = shift; + my $active = $self->stash ('msc_menu_active'); + my $result; + my $status = 'inactive'; + $status = 'active' if ($active eq 'dashboard'); + $result .= "dashboard"; + foreach my $world (&getEnabledWorlds, &getDisabledWorlds) { + $status = 'inactive'; + $status = 'active' if ($active eq $world); + $result .= "$world"; + } + return new Mojo::ByteStream ($result); +}; + +=head2 HTTP Request Methods + +=head3 get / + + Renders the dashboard. + +=cut + +get '/' => sub { + my $self = shift; + $self->stash (msc_menu_active => 'dashboard'); + $self->render (template => "dashboard", title => "MSC GUI: Dashboard"); +}; + +=head3 get /worlds + + Renders a list of worlds. + +=cut + +get '/worlds' => sub { + my $self = shift; + $self->render (template => "worlds", title => "MSC GUI: World List"); +}; + +=head3 get /worlds/ + + Renders a specific world. + +=cut + +get '/worlds/:world' => sub { + my $self = shift; + my $world = $self->stash ('world'); + $self->stash (msc_menu_active => $world); + my %enabledWorlds = map { $_ => 1 } &getEnabledWorlds; + if (defined $enabledWorlds{$world}) { + my $backups = mscs ("list-backups", $world); + my @query = split /\t/, mscs ("query", $world); + $self->stash (msc_backup_list => $backups); + $self->stash (msc_query_motd => $query[6]); + $self->stash (msc_query_gametype => $query[8]); + $self->stash (msc_query_gameid => $query[10]); + $self->stash (msc_query_version => $query[12]); + $self->stash (msc_query_plugins => $query[14]); + $self->stash (msc_query_map => $query[16]); + $self->stash (msc_query_numplayers => $query[18]); + $self->stash (msc_query_maxplayers => $query[20]); + $self->stash (msc_query_hostport => $query[22]); + $self->stash (msc_query_hostip => $query[24]); + $self->stash (msc_query_players => $query[29]); + $self->render (template => "enabled-world", title => "MSC GUI: $world"); + } + else { + $self->render (template => 'disabled-world', title => "MSC GUI: $world"); + } +}; + +# Start the MSC-GUI app. +app->start; + +=head2 Internal Methods + +=head3 getEnabledWorlds + + Returns a list of enabled worlds. + +=cut + +sub getEnabledWorlds { + my $self = shift; + my @worlds; + foreach my $line (mscs ("ls enabled")) { + if ($line =~ /^\s*(\w+):/) { + push @worlds, $1; + } + } + return @worlds; +} + +=head3 getDisabledWorlds + + Returns a list of disabled worlds. + +=cut + +sub getDisabledWorlds { + my $self = shift; + my @worlds; + foreach my $line (mscs ("ls disabled")) { + if ($line =~ /^\s*(\w+):/) { + push @worlds, $1; + } + } + return @worlds; +} + +=head3 mscs + + Run the mscs command and return the output. + +=cut + +sub mscs { + return `mscs @_`; +} + diff --git a/msc-gui.service b/msc-gui.service new file mode 100644 index 0000000..96cf0eb --- /dev/null +++ b/msc-gui.service @@ -0,0 +1,16 @@ +[Unit] +Description=Minecraft Server Control GUI +Documentation=https://github.com/MinecraftServerControl/msc-gui +Requires=network.target +After=network.target + +[Service] +User=minecraft +Group=minecraft +PIDFile=/opt/mscs/gui/hypnotoad.pid +ExecStart=/usr/bin/hypnotoad /opt/mscs/gui/msc-gui +ExecReload=/usr/bin/hypnotoad /opt/mscs/gui/msc-gui +KillMode=process + +[Install] +WantedBy=multi-user.target diff --git a/themes/default/favicon.ico b/public/favicon.ico similarity index 100% rename from themes/default/favicon.ico rename to public/favicon.ico diff --git a/themes/default/stylesheet.css b/public/stylesheet.css similarity index 69% rename from themes/default/stylesheet.css rename to public/stylesheet.css index 2078207..a6910c0 100644 --- a/themes/default/stylesheet.css +++ b/public/stylesheet.css @@ -1,11 +1,20 @@ body { - background-color: #aaaaaa; + background-color: #ababab; width: 100%; margin: 0px; } +#body_box { + background-color: #5d6e7f; + position: absolute; + width: 1000px; + height: 100%; + left: 0px; + top: 0px; + z-index: 0; +} + #title_box { - background-color: #666666; background-image: url("title-background.png"); background-repeat: no-repeat; position: absolute; @@ -17,7 +26,7 @@ body { } #title { - position:absolute; + position: absolute; left: 130px; top: 0px; margin-top: 25px; @@ -45,7 +54,21 @@ body { z-index: 1; } -a.menu_item { +a.menu_item_active { + display: inline-block; + text-decoration: none; + text-shadow: 0px 1px 0px #aaaaaa; + color: #000000; + font-family: "Arial", sans-serif; + font-size: 20px; + padding: 5px 10px 5px 10px; + text-transform: capitalize; + border-radius: 10px 10px 0px 0px; + background-image: linear-gradient(to bottom, #6eb5ff, #5d6e7f); + z-index: 1; +} + +a.menu_item_inactive { display: inline-block; text-decoration: none; text-shadow: 0px 1px 0px #aaaaaa; @@ -55,16 +78,15 @@ a.menu_item { padding: 5px 10px 5px 10px; text-transform: capitalize; border-radius: 10px 10px 0px 0px; - background-image: linear-gradient(to bottom, #999999, #666666); + background-image: linear-gradient(to bottom, #5891cc, #5d6e7f); z-index: 1; } #content_box { - background-color: #666666; position: absolute; - width: 1000px; + width: 980px; height: 500px; - left: 0px; + left: 20px; top: 150px; margin: 0px; z-index: 0; @@ -77,7 +99,7 @@ a.menu_item { #map { border-style: solid; position: absolute; - left: 600px; + left: 580px; top: 0px; } diff --git a/themes/default/title-background.png b/public/title-background.png similarity index 100% rename from themes/default/title-background.png rename to public/title-background.png diff --git a/themes/default/title-image.png b/public/title-image.png similarity index 100% rename from themes/default/title-image.png rename to public/title-image.png diff --git a/templates/dashboard.html.ep b/templates/dashboard.html.ep new file mode 100644 index 0000000..d5155a8 --- /dev/null +++ b/templates/dashboard.html.ep @@ -0,0 +1,7 @@ +% layout 'default'; + +

    Dashboard

    +

    Enabled Worlds:

    +<%= msc_enabled_world_list %> +

    Disabled Worlds:

    +<%= msc_disabled_world_list %> diff --git a/templates/disabled-world.html.ep b/templates/disabled-world.html.ep new file mode 100644 index 0000000..162e406 --- /dev/null +++ b/templates/disabled-world.html.ep @@ -0,0 +1,3 @@ +% layout 'default'; + +

    Disabled world: <%= $world %>

    diff --git a/templates/enabled-world.html.ep b/templates/enabled-world.html.ep new file mode 100644 index 0000000..1028778 --- /dev/null +++ b/templates/enabled-world.html.ep @@ -0,0 +1,29 @@ +% layout 'default'; + +

    <%= $world %>

    + +
    +List of backups:
    +
    +<%= $msc_backup_list %>
    +
    + +
    +Query:
    +
    +MOTD: <%= $msc_query_motd %>
    +Game type: <%= $msc_query_gametype %>
    +Game ID: <%= $msc_query_gameid %>
    +Server Version: <%= $msc_query_version %>
    +Server Plugins: <%= $msc_query_plugins %>
    +Server Map: <%= $msc_query_map %>
    +Number of players online: <%= $msc_query_numplayers %>
    +Maximum players: <%= $msc_query_maxplayers %>
    +Host port: <%= $msc_query_hostport %>
    +Host IP: <%= $msc_query_hostip %>
    +Player list: <%= $msc_query_players %>
    +
    + + +Map of <%= $world %> + diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep new file mode 100644 index 0000000..5d907e5 --- /dev/null +++ b/templates/layouts/default.html.ep @@ -0,0 +1,18 @@ + + + +<%= title %> + + + + +
    +
    +Title Image +
    +
    Minecraft Server Control
    + +
    <%= content %>
    +
    + + diff --git a/templates/worlds.html.ep b/templates/worlds.html.ep new file mode 100644 index 0000000..c8ca52d --- /dev/null +++ b/templates/worlds.html.ep @@ -0,0 +1,6 @@ +% layout 'default'; + +

    Enabled Worlds:

    +<%= msc_enabled_world_list %> +

    Disabled Worlds:

    +<%= msc_disabled_world_list %> diff --git a/themes/default/disabled-world.xhtml b/themes/default/disabled-world.xhtml deleted file mode 100644 index ba08719..0000000 --- a/themes/default/disabled-world.xhtml +++ /dev/null @@ -1,3 +0,0 @@ -

    - -

    This world is disabled.

    diff --git a/themes/default/index.xhtml b/themes/default/index.xhtml deleted file mode 100644 index e446a34..0000000 --- a/themes/default/index.xhtml +++ /dev/null @@ -1,17 +0,0 @@ - - - -<?msc_title?> - - - - - -
    -Title Image -
    -
    - -
    - - diff --git a/themes/default/world.xhtml b/themes/default/world.xhtml deleted file mode 100644 index 43834c7..0000000 --- a/themes/default/world.xhtml +++ /dev/null @@ -1,25 +0,0 @@ -

    - -
    -List of backups:
    -
    -
    -
    - -
    -Query:
    -
    -MOTD: 
    -Game type: 
    -Game ID: 
    -Server Version: 
    -Server Plugins: 
    -Server Map: 
    -Number of players online: 
    -Maximum players: 
    -Host port: 
    -Host IP: 
    -Player list: 
    -
    - -Map of <?msc_world_name?> From 283841ad835911eddd1733042ad6d8181a05aee3 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Wed, 6 Jun 2018 12:38:04 -0700 Subject: [PATCH 02/15] README: add how to install Mojolicious --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 85701fa..f90ad94 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,17 @@ usable state, this message will be removed. ## Prerequisites for installation -The Minecraft Server Control GUI uses Perl and Mojolicious, a Perl-based web -framework, to present a web-based interface to the +The Minecraft Server Control GUI uses Perl and +[Mojolicious](https://mojolicious.org/), a Perl-based web framework, to +present a web-based interface to the [Minecraft Server Control Script](https://github.com/MinecraftServerControl/mscs). As such, the `mscs` script must be [installed](https://github.com/MinecraftServerControl/mscs/blob/master/README.md#installation) -and working for the GUI to function. +and working for the GUI to function. Likewise, Mojolicious must be installed +for MSC-GUI to function. If you are running Debian or Ubuntu, you can make +sure that Mojolicious is installed by running: + + sudo apt install libmojolicious-perl ## Installation From 3be65561f0b4540c67ef5d71ebaee6c3efd122a7 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 00:42:36 -0700 Subject: [PATCH 03/15] msc-gui: move title to stash --- msc-gui | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/msc-gui b/msc-gui index 1e988e2..bbf7cc4 100644 --- a/msc-gui +++ b/msc-gui @@ -130,8 +130,9 @@ helper 'msc_menu' => sub { get '/' => sub { my $self = shift; + $self->stash (title => "MSC GUI: Dashboard"); $self->stash (msc_menu_active => 'dashboard'); - $self->render (template => "dashboard", title => "MSC GUI: Dashboard"); + $self->render (template => "dashboard"); }; =head3 get /worlds @@ -142,7 +143,8 @@ get '/' => sub { get '/worlds' => sub { my $self = shift; - $self->render (template => "worlds", title => "MSC GUI: World List"); + $self->stash (title => "MSC GUI: World List"); + $self->render (template => "worlds"); }; =head3 get /worlds/ @@ -154,6 +156,7 @@ get '/worlds' => sub { get '/worlds/:world' => sub { my $self = shift; my $world = $self->stash ('world'); + $self->stash (title => "MSC GUI: $world"); $self->stash (msc_menu_active => $world); my %enabledWorlds = map { $_ => 1 } &getEnabledWorlds; if (defined $enabledWorlds{$world}) { @@ -171,10 +174,10 @@ get '/worlds/:world' => sub { $self->stash (msc_query_hostport => $query[22]); $self->stash (msc_query_hostip => $query[24]); $self->stash (msc_query_players => $query[29]); - $self->render (template => "enabled-world", title => "MSC GUI: $world"); + $self->render (template => "enabled-world"); } else { - $self->render (template => 'disabled-world', title => "MSC GUI: $world"); + $self->render (template => 'disabled-world'); } }; From 09c178d0b08ca596189de3d36431c6cac8973aa6 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 00:44:10 -0700 Subject: [PATCH 04/15] msc-gui: add enabled and disabled world list to stash --- msc-gui | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/msc-gui b/msc-gui index bbf7cc4..5e06cd5 100644 --- a/msc-gui +++ b/msc-gui @@ -131,6 +131,8 @@ helper 'msc_menu' => sub { get '/' => sub { my $self = shift; $self->stash (title => "MSC GUI: Dashboard"); + $self->stash (msc_enabled_worlds => [ &getEnabledWorlds ]); + $self->stash (msc_disabled_worlds => [ &getDisabledWorlds ]); $self->stash (msc_menu_active => 'dashboard'); $self->render (template => "dashboard"); }; @@ -144,6 +146,8 @@ get '/' => sub { get '/worlds' => sub { my $self = shift; $self->stash (title => "MSC GUI: World List"); + $self->stash (msc_enabled_worlds => [ &getEnabledWorlds ]); + $self->stash (msc_disabled_worlds => [ &getDisabledWorlds ]); $self->render (template => "worlds"); }; From 3ed18445aae7e0786fd718ea5c54d466f88b4d2a Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 00:44:52 -0700 Subject: [PATCH 05/15] templates: use enabled and disabled world list from stash --- templates/dashboard.html.ep | 14 ++++++++++++-- templates/worlds.html.ep | 13 +++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/templates/dashboard.html.ep b/templates/dashboard.html.ep index d5155a8..0a3bf70 100644 --- a/templates/dashboard.html.ep +++ b/templates/dashboard.html.ep @@ -1,7 +1,17 @@ % layout 'default';

    Dashboard

    +

    Enabled Worlds:

    -<%= msc_enabled_world_list %> +
      +% foreach (@{stash ('msc_enabled_worlds')}) { +
    • <%= $_ %>
    • +% } +
    +

    Disabled Worlds:

    -<%= msc_disabled_world_list %> +
      +% foreach (@{stash ('msc_disabled_worlds')}) { +
    • <%= $_ %>
    • +% } +
    diff --git a/templates/worlds.html.ep b/templates/worlds.html.ep index c8ca52d..ac2bb4f 100644 --- a/templates/worlds.html.ep +++ b/templates/worlds.html.ep @@ -1,6 +1,15 @@ % layout 'default';

    Enabled Worlds:

    -<%= msc_enabled_world_list %> +
      +% foreach (@{stash ('msc_enabled_worlds')}) { +
    • <%= $_ %>
    • +% } +
    +

    Disabled Worlds:

    -<%= msc_disabled_world_list %> +
      +% foreach (@{stash ('msc_disabled_worlds')}) { +
    • <%= $_ %>
    • +% } +
    From 714bf14a05fca43c71969a30a1fff1ba7ee36e46 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 00:47:35 -0700 Subject: [PATCH 06/15] msc-gui: drop unused helpers --- msc-gui | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/msc-gui b/msc-gui index 5e06cd5..0da9dbc 100644 --- a/msc-gui +++ b/msc-gui @@ -71,34 +71,6 @@ use Mojo::ByteStream; =head2 Helper Functions -=head3 msc_enabled_world_list - - Returns an unordered list in HTML format containing enabled worlds. - -=cut - -helper 'msc_enabled_world_list' => sub { - my $self = shift; - my $result = join "\n", map { - "
  • $_
  • " - } &getEnabledWorlds; - return new Mojo::ByteStream ("
      \n$result\n
    "); -}; - -=head3 msc_disabled_world_list - - Returns an unordered list in HTML format containing disabled worlds. - -=cut - -helper 'msc_disabled_world_list' => sub { - my $self = shift; - my $result = join "\n", map { - "
  • $_
  • " - } &getDisabledWorlds; - return new Mojo::ByteStream ("
      \n$result\n
    "); -}; - =head3 msc_menu Returns the menu in HTML format. From 98f77bfe41e5f3544531166c97b68198e80829d4 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 00:48:14 -0700 Subject: [PATCH 07/15] templates: add worlds.json --- templates/worlds.json.ep | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 templates/worlds.json.ep diff --git a/templates/worlds.json.ep b/templates/worlds.json.ep new file mode 100644 index 0000000..69a38d3 --- /dev/null +++ b/templates/worlds.json.ep @@ -0,0 +1,6 @@ +% use Mojo::JSON qw (to_json); +% my $json = { +% enabled_worlds => stash ('msc_enabled_worlds'), +% disabled_worlds => stash ('msc_disabled_worlds') +% }; +%== to_json $json From 65fd8ca63ddcda54835038b974e8e27b016192b6 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 16:10:25 -0700 Subject: [PATCH 08/15] msc-gui: add query for each world to the dashboard stash --- msc-gui | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/msc-gui b/msc-gui index 0da9dbc..708d8be 100644 --- a/msc-gui +++ b/msc-gui @@ -68,6 +68,7 @@ use feature ':5.10'; use Mojolicious::Lite; use Mojo::ByteStream; +use Mojo::JSON qw(decode_json); =head2 Helper Functions @@ -102,9 +103,16 @@ helper 'msc_menu' => sub { get '/' => sub { my $self = shift; + my @enabled = &getEnabledWorlds; + my @disabled = &getDisabledWorlds; + my %query; + foreach my $world (@enabled) { + $query{$world} = decode_json mscs ("query-json", $world); + } $self->stash (title => "MSC GUI: Dashboard"); - $self->stash (msc_enabled_worlds => [ &getEnabledWorlds ]); - $self->stash (msc_disabled_worlds => [ &getDisabledWorlds ]); + $self->stash (msc_enabled_worlds => \@enabled); + $self->stash (msc_disabled_worlds => \@disabled); + $self->stash (msc_query => \%query); $self->stash (msc_menu_active => 'dashboard'); $self->render (template => "dashboard"); }; From dbd324808a22bf4601cf13ff4f695f0d83170249 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 16:11:22 -0700 Subject: [PATCH 09/15] layouts: add bootstrap to the default layout --- templates/layouts/default.html.ep | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep index 5d907e5..90ec529 100644 --- a/templates/layouts/default.html.ep +++ b/templates/layouts/default.html.ep @@ -4,6 +4,8 @@ <%= title %> + +
    From 402d136c842aa05383f8f67fcd89451247aca402 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 16:16:20 -0700 Subject: [PATCH 10/15] templates: use Roflicide's boostrap template --- templates/dashboard.html.ep | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/templates/dashboard.html.ep b/templates/dashboard.html.ep index 0a3bf70..74eff9d 100644 --- a/templates/dashboard.html.ep +++ b/templates/dashboard.html.ep @@ -1,17 +1,27 @@ % layout 'default'; -

    Dashboard

    - -

    Enabled Worlds:

    -
      -% foreach (@{stash ('msc_enabled_worlds')}) { -
    • <%= $_ %>
    • +
      +% my %query = %{stash ('msc_query')}; +% foreach my $world (@{stash ('msc_enabled_worlds')}) { +
      +
      + +
      +

      <%= $world %>

      +

      <%= $query{$world}->{motd} %>

      +
      +
      +
      % } -
    - -

    Disabled Worlds:

    -
      -% foreach (@{stash ('msc_disabled_worlds')}) { -
    • <%= $_ %>
    • +% foreach my $world (@{stash ('msc_disabled_worlds')}) { +
      +
      + +
      +

      <%= $world %>

      +

      Disabled

      +
      +
      +
      % } -
    +
    From ae837fd811152725d7b6a5a07f9d8d7808e0840e Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 16:20:11 -0700 Subject: [PATCH 11/15] templates: better match Roflicide's boostrap template --- templates/dashboard.html.ep | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/dashboard.html.ep b/templates/dashboard.html.ep index 74eff9d..b43b424 100644 --- a/templates/dashboard.html.ep +++ b/templates/dashboard.html.ep @@ -5,7 +5,7 @@ % foreach my $world (@{stash ('msc_enabled_worlds')}) {
    - +

    <%= $world %>

    <%= $query{$world}->{motd} %>

    @@ -16,7 +16,7 @@ % foreach my $world (@{stash ('msc_disabled_worlds')}) {
    - +

    <%= $world %>

    Disabled

    From 7fa4f90ba04516ed63282d84efcada12447b6f3d Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 20:29:04 -0700 Subject: [PATCH 12/15] msc-gui: use the new status-json msctl option --- msc-gui | 23 +++++------------------ templates/dashboard.html.ep | 26 +++++++++++++++++++++----- templates/enabled-world.html.ep | 28 +++++++++++++++++----------- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/msc-gui b/msc-gui index 708d8be..7be4a72 100644 --- a/msc-gui +++ b/msc-gui @@ -105,14 +105,11 @@ get '/' => sub { my $self = shift; my @enabled = &getEnabledWorlds; my @disabled = &getDisabledWorlds; - my %query; - foreach my $world (@enabled) { - $query{$world} = decode_json mscs ("query-json", $world); - } + my $status = decode_json mscs ("status-json"); $self->stash (title => "MSC GUI: Dashboard"); $self->stash (msc_enabled_worlds => \@enabled); $self->stash (msc_disabled_worlds => \@disabled); - $self->stash (msc_query => \%query); + $self->stash (msc_status => $status); $self->stash (msc_menu_active => 'dashboard'); $self->render (template => "dashboard"); }; @@ -140,24 +137,14 @@ get '/worlds' => sub { get '/worlds/:world' => sub { my $self = shift; my $world = $self->stash ('world'); + my $status = decode_json mscs ("status-json $world"); $self->stash (title => "MSC GUI: $world"); $self->stash (msc_menu_active => $world); + $self->stash (msc_status => $status); my %enabledWorlds = map { $_ => 1 } &getEnabledWorlds; if (defined $enabledWorlds{$world}) { my $backups = mscs ("list-backups", $world); - my @query = split /\t/, mscs ("query", $world); - $self->stash (msc_backup_list => $backups); - $self->stash (msc_query_motd => $query[6]); - $self->stash (msc_query_gametype => $query[8]); - $self->stash (msc_query_gameid => $query[10]); - $self->stash (msc_query_version => $query[12]); - $self->stash (msc_query_plugins => $query[14]); - $self->stash (msc_query_map => $query[16]); - $self->stash (msc_query_numplayers => $query[18]); - $self->stash (msc_query_maxplayers => $query[20]); - $self->stash (msc_query_hostport => $query[22]); - $self->stash (msc_query_hostip => $query[24]); - $self->stash (msc_query_players => $query[29]); + $self->stash (msc_backup_list => $backups); $self->render (template => "enabled-world"); } else { diff --git a/templates/dashboard.html.ep b/templates/dashboard.html.ep index b43b424..5e50408 100644 --- a/templates/dashboard.html.ep +++ b/templates/dashboard.html.ep @@ -1,14 +1,27 @@ % layout 'default';
    -% my %query = %{stash ('msc_query')}; +% my $status = stash ('msc_status'); % foreach my $world (@{stash ('msc_enabled_worlds')}) {
    -

    <%= $world %>

    -

    <%= $query{$world}->{motd} %>

    +

    + <%= $world %> + % if (defined $status->{$world}->{running} && $status->{$world}->{running}) { + Online + % } + % else { + Offline + % } +

    +

    + % if (defined $status->{$world}->{query}->{version}) { + <%= $status->{$world}->{query}->{numplayers} %> / <%= $status->{$world}->{query}->{maxplayers} %> Players Online
    + <%= $status->{$world}->{query}->{version} %> + % } +

    @@ -18,8 +31,11 @@
    -

    <%= $world %>

    -

    Disabled

    +

    + <%= $world %> + Disabled +

    +

    diff --git a/templates/enabled-world.html.ep b/templates/enabled-world.html.ep index 1028778..669c67e 100644 --- a/templates/enabled-world.html.ep +++ b/templates/enabled-world.html.ep @@ -1,5 +1,11 @@ % layout 'default'; +% my $status = stash ('msc_status'); +% my @players = (); +% if (defined $status->{$world}->{query}->{players}) { +% @players = @{$status->{$world}->{query}->{players}}; +% } +

    <%= $world %>

    @@ -11,17 +17,17 @@ List of backups:
     
     Query:
     
    -MOTD: <%= $msc_query_motd %>
    -Game type: <%= $msc_query_gametype %>
    -Game ID: <%= $msc_query_gameid %>
    -Server Version: <%= $msc_query_version %>
    -Server Plugins: <%= $msc_query_plugins %>
    -Server Map: <%= $msc_query_map %>
    -Number of players online: <%= $msc_query_numplayers %>
    -Maximum players: <%= $msc_query_maxplayers %>
    -Host port: <%= $msc_query_hostport %>
    -Host IP: <%= $msc_query_hostip %>
    -Player list: <%= $msc_query_players %>
    +MOTD: <%= $status->{$world}->{query}->{motd} %>
    +Game type: <%= $status->{$world}->{query}->{gametype} %>
    +Game ID: <%= $status->{$world}->{query}->{gameid} %>
    +Server Version: <%= $status->{$world}->{query}->{version} %>
    +Server Plugins: <%= $status->{$world}->{query}->{plugins} %>
    +Server Map: <%= $status->{$world}->{query}->{map} %>
    +Number of players online: <%= $status->{$world}->{query}->{numplayers} %>
    +Maximum players: <%= $status->{$world}->{query}->{maxplayers} %>
    +Host port: <%= $status->{$world}->{query}->{hostport} %>
    +Host IP: <%= $status->{$world}->{query}->{hostip} %>
    +Player list: <%= join ',', @players %>
     
    From 4b7e30637520e78a9ce761232c815d410c4b0341 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 20:49:51 -0700 Subject: [PATCH 13/15] templates: display memory used in dashboard --- templates/dashboard.html.ep | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/dashboard.html.ep b/templates/dashboard.html.ep index 5e50408..7d777dd 100644 --- a/templates/dashboard.html.ep +++ b/templates/dashboard.html.ep @@ -19,7 +19,10 @@

    % if (defined $status->{$world}->{query}->{version}) { <%= $status->{$world}->{query}->{numplayers} %> / <%= $status->{$world}->{query}->{maxplayers} %> Players Online
    - <%= $status->{$world}->{query}->{version} %> + <%= $status->{$world}->{query}->{version} %>
    + % } + % if (defined $status->{$world}->{running} && $status->{$world}->{running}) { + <%= reverse join ',', unpack '(A3)*', reverse $status->{$world}->{memory} %> kB Memory Used % }

    From 5a1cbc7fb8862a8fd393a1933744ed33240e9a4f Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 23:51:17 -0700 Subject: [PATCH 14/15] stylesheet: remove left padding on content --- public/stylesheet.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/stylesheet.css b/public/stylesheet.css index a6910c0..9ef37d9 100644 --- a/public/stylesheet.css +++ b/public/stylesheet.css @@ -86,7 +86,7 @@ a.menu_item_inactive { position: absolute; width: 980px; height: 500px; - left: 20px; + left: 0px; top: 150px; margin: 0px; z-index: 0; From a5e0ad6c84c9a4cceb73d8668305c22a073fa3f0 Mon Sep 17 00:00:00 2001 From: "Jason M. Wood" Date: Sat, 23 Jun 2018 23:51:52 -0700 Subject: [PATCH 15/15] dashboard: wrap content in container --- templates/dashboard.html.ep | 66 +++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/templates/dashboard.html.ep b/templates/dashboard.html.ep index 7d777dd..00fb66a 100644 --- a/templates/dashboard.html.ep +++ b/templates/dashboard.html.ep @@ -1,46 +1,48 @@ % layout 'default'; -
    % my $status = stash ('msc_status'); -% foreach my $world (@{stash ('msc_enabled_worlds')}) { -
    -
    - -
    -

    - <%= $world %> - % if (defined $status->{$world}->{running} && $status->{$world}->{running}) { - Online - % } - % else { - Offline - % } -

    -

    +

    +
    + % foreach my $world (@{stash ('msc_enabled_worlds')}) { +
    +
    + +
    +

    + <%= $world %> + % if ($status->{$world}->{running}) { + Online + % } + % else { + Offline + % } +

    +

    % if (defined $status->{$world}->{query}->{version}) { <%= $status->{$world}->{query}->{numplayers} %> / <%= $status->{$world}->{query}->{maxplayers} %> Players Online
    - <%= $status->{$world}->{query}->{version} %>
    + Version <%= $status->{$world}->{query}->{version} %>
    % } - % if (defined $status->{$world}->{running} && $status->{$world}->{running}) { + % if ($status->{$world}->{running}) { <%= reverse join ',', unpack '(A3)*', reverse $status->{$world}->{memory} %> kB Memory Used % } -

    +

    +
    -
    -% } -% foreach my $world (@{stash ('msc_disabled_worlds')}) { -
    -
    - -
    -

    - <%= $world %> - Disabled -

    -

    + % } + % foreach my $world (@{stash ('msc_disabled_worlds')}) { +
    +
    + +
    +

    + <%= $world %> + Disabled +

    +

    +
    + % }
    -% }