Allow files to be uploaded to the ASL Scenario Archive.

master
Pacman Ghost 4 years ago
parent 63ceda1044
commit de8f39fa95
  1. 8
      vasl_templates/main_window.py
  2. 3
      vasl_templates/webapp/__init__.py
  3. 23
      vasl_templates/webapp/downloads.py
  4. 5
      vasl_templates/webapp/main.py
  5. 255
      vasl_templates/webapp/scenarios.py
  6. 12
      vasl_templates/webapp/static/css/scenario-card.css
  7. 20
      vasl_templates/webapp/static/css/scenario-downloads-dialog.css
  8. 2
      vasl_templates/webapp/static/css/scenario-search-dialog.css
  9. 56
      vasl_templates/webapp/static/css/scenario-upload-dialog.css
  10. 48
      vasl_templates/webapp/static/imageZoom/jquery.imageZoom.css
  11. 1
      vasl_templates/webapp/static/imageZoom/jquery.imageZoom.min.js
  12. BIN
      vasl_templates/webapp/static/imageZoom/jquery.imageZoom.png
  13. BIN
      vasl_templates/webapp/static/images/download.png
  14. BIN
      vasl_templates/webapp/static/images/screenshot.png
  15. BIN
      vasl_templates/webapp/static/images/upload.png
  16. BIN
      vasl_templates/webapp/static/images/vassal-screenshot-hint.png
  17. 4
      vasl_templates/webapp/static/lfa-upload.js
  18. 1
      vasl_templates/webapp/static/lightgallery/css/lightgallery.min.css
  19. 51
      vasl_templates/webapp/static/lightgallery/fonts/lg.svg
  20. BIN
      vasl_templates/webapp/static/lightgallery/fonts/lg.ttf
  21. BIN
      vasl_templates/webapp/static/lightgallery/fonts/lg.woff
  22. BIN
      vasl_templates/webapp/static/lightgallery/img/loading.gif
  23. 4
      vasl_templates/webapp/static/lightgallery/js/lg-rotate.min.js
  24. 4
      vasl_templates/webapp/static/lightgallery/js/lg-thumbnail.min.js
  25. 4
      vasl_templates/webapp/static/lightgallery/js/lg-zoom.min.js
  26. 4
      vasl_templates/webapp/static/lightgallery/js/lightgallery.min.js
  27. 6
      vasl_templates/webapp/static/roar.js
  28. 454
      vasl_templates/webapp/static/scenario-upload.js
  29. 314
      vasl_templates/webapp/static/scenarios.js
  30. 9
      vasl_templates/webapp/static/snippets.js
  31. 1
      vasl_templates/webapp/static/sortable.js
  32. 54
      vasl_templates/webapp/static/utils.js
  33. 9
      vasl_templates/webapp/static/vassal.js
  34. 1
      vasl_templates/webapp/static/vo.js
  35. 16
      vasl_templates/webapp/templates/index.html
  36. 9
      vasl_templates/webapp/templates/scenario-card.html
  37. 5
      vasl_templates/webapp/templates/scenario-downloads-dialog.html
  38. 6
      vasl_templates/webapp/templates/scenario-search-dialog.html
  39. 66
      vasl_templates/webapp/templates/scenario-upload-dialog.html
  40. 6
      vasl_templates/webapp/tests/fixtures/asa-responses/incorrect-token.json
  41. 6
      vasl_templates/webapp/tests/fixtures/asa-responses/missing-token.json
  42. 6
      vasl_templates/webapp/tests/fixtures/asa-responses/missing-user-name.json
  43. 6
      vasl_templates/webapp/tests/fixtures/asa-responses/no-files-uploaded.json
  44. 6
      vasl_templates/webapp/tests/fixtures/asa-responses/ok.json
  45. 23
      vasl_templates/webapp/tests/fixtures/asl-scenario-archive.json
  46. 25
      vasl_templates/webapp/tests/remote.py
  47. 194
      vasl_templates/webapp/tests/test_scenario_search.py
  48. 27
      vasl_templates/webapp/utils.py
  49. 12
      vasl_templates/webapp/vassal.py
  50. 10
      vasl_templates/webapp/webdriver.py
  51. BIN
      vassal-shim/release/vassal-shim.jar
  52. 56
      vassal-shim/src/vassal_shim/AppImageSaver.java
  53. 12
      vassal-shim/src/vassal_shim/Main.java
  54. 102
      vassal-shim/src/vassal_shim/VassalShim.java

@ -334,3 +334,11 @@ class MainWindow( QWidget ):
def on_snippet_image( self, img_data ):
"""Called when a snippet image has been generated."""
self._web_channel_handler.on_snippet_image( img_data )
@pyqtSlot( str, str, result=bool )
@catch_exceptions( caption="SLOT EXCEPTION", retval=False )
def save_downloaded_vsav( self, fname, data ):
"""Called when a VASL scenario has been downloaded."""
data = base64.b64decode( data )
# NOTE: We handle this the same as saving an updated VSAV.
return self._web_channel_handler.save_updated_vsav( fname, data )

@ -83,6 +83,9 @@ app = Flask( __name__ )
app.config[ "ASA_SCENARIO_URL" ] = "https://aslscenarioarchive.com/scenario.php?id={ID}"
app.config[ "ASA_PUBLICATION_URL" ] = "https://aslscenarioarchive.com/viewPub.php?id={ID}"
app.config[ "ASA_PUBLISHER_URL" ] = "https://aslscenarioarchive.com/viewPublisher.php?id={ID}"
app.config[ "ASA_GET_SCENARIO_URL" ] = "https://aslscenarioarchive.com/rest/scenario/list/{ID}"
app.config[ "ASA_MAX_VASL_SETUP_SIZE" ] = 200 # nb: KB
app.config[ "ASA_MAX_SCREENSHOT_SIZE" ] = 200 # nb: KB
# load the application configuration
config_dir = os.path.join( BASE_DIR, "config" )

@ -46,14 +46,14 @@ class DownloadedFile:
_registry.add( self )
# check if we have a cached copy of the file
self._cache_fname = os.path.join( tempfile.gettempdir(), "vasl-templates."+fname )
if os.path.isfile( self._cache_fname ):
self.cache_fname = os.path.join( tempfile.gettempdir(), "vasl-templates."+fname )
if os.path.isfile( self.cache_fname ):
# yup - load it
_logger.info( "Using cached %s file: %s", key, self._cache_fname )
self._set_data( self._cache_fname )
_logger.info( "Using cached %s file: %s", key, self.cache_fname )
self._set_data( self.cache_fname )
else:
# nope - start with an empty data set
_logger.debug( "No cached %s file: %s", key, self._cache_fname )
_logger.debug( "No cached %s file: %s", key, self.cache_fname )
def _set_data( self, data ):
"""Install a new data set."""
@ -114,9 +114,9 @@ class DownloadedFile:
_logger.info( "Download of the %s file has been disabled.", df.key )
continue
ttl *= 60*60
if os.path.isfile( df._cache_fname ):
if os.path.isfile( df.cache_fname ):
# yup - check how long ago it was downloaded
mtime = os.path.getmtime( df._cache_fname )
mtime = os.path.getmtime( df.cache_fname )
age = int( time.time() - mtime )
_logger.debug( "Checking the cached %s file: age=%s, ttl=%s (mtime=%s)",
df.key,
@ -130,7 +130,10 @@ class DownloadedFile:
# download the file
_logger.info( "Downloading the %s file: %s", df.key, url )
try:
fp = urllib.request.urlopen( url )
req = urllib.request.Request( url,
headers = { "Accept-Encoding": "gzip, deflate" }
)
fp = urllib.request.urlopen( req )
data = fp.read().decode( "utf-8" )
except Exception as ex: #pylint: disable=broad-except
msg = str( getattr(ex,"reason",None) or ex )
@ -150,6 +153,6 @@ class DownloadedFile:
# is performance-critical and we can probably live it.
# save a cached copy of the data
_logger.debug( "Saving a cached copy of the %s file: %s", df.key, df._cache_fname )
with open( df._cache_fname, "w", encoding="utf-8" ) as fp:
_logger.debug( "Saving a cached copy of the %s file: %s", df.key, df.cache_fname )
with open( df.cache_fname, "w", encoding="utf-8" ) as fp:
fp.write( data )

@ -68,6 +68,7 @@ _APP_CONFIG_DEFAULTS = { # Bodhgaya, India (APR/19)
# but VASSAL is already so slow to load images, and doing everything twice would make it that much worse :-/
"ONLINE_COUNTER_IMAGES_URL_TEMPLATE": "https://raw.githubusercontent.com/vasl-developers/vasl/develop/dist/images/{{PATH}}", #pylint: disable=line-too-long
"ONLINE_EXTN_COUNTER_IMAGES_URL_TEMPLATE": "http://vasl-templates.org/services/counter/{{EXTN_ID}}/{{PATH}}",
"ASA_UPLOAD_URL": "https://aslscenarioarchive.com/rest/update/{ID}?user={USER}&token={TOKEN}",
}
@app.route( "/app-config" )
@ -95,6 +96,10 @@ def get_app_config():
for key in ["APP_NAME","APP_VERSION","APP_DESCRIPTION","APP_HOME_URL"]:
vals[ key ] = getattr( vasl_templates.webapp.config.constants, key )
# include the ASL Scenario Archive config
for key in ["ASA_MAX_VASL_SETUP_SIZE","ASA_MAX_SCREENSHOT_SIZE"]:
vals[ key ] = app.config[ key ]
# include the dice hotness config
vals[ "LFA_DICE_HOTNESS_WEIGHTS" ] = get_json_val(
"LFA_DICE_HOTNESS_WEIGHTS", DEFAULT_LFA_DICE_HOTNESS_WEIGHTS

@ -3,13 +3,24 @@
# NOTE: Disable "DownloadedFile has no 'index' member" warnings.
#pylint: disable=no-member
import os
import json
import urllib.request
import base64
import re
import time
import math
import logging
from flask import request, render_template, jsonify, abort
from PIL import Image, ImageOps
from vasl_templates.webapp import app
from vasl_templates.webapp.downloads import DownloadedFile
from vasl_templates.webapp.utils import get_month_name, make_formatted_day_of_month, friendly_fractions, parse_int
from vasl_templates.webapp.vassal import VassalShim
from vasl_templates.webapp.utils import TempFile, \
get_month_name, make_formatted_day_of_month, friendly_fractions, parse_int, \
trim_image, get_image_data, remove_alpha_from_image
# ---------------------------------------------------------------------
@ -97,7 +108,7 @@ def get_scenario_index():
with _asa_scenarios:
if _asa_scenarios.index is None:
return _make_not_available_response(
"The scenario index is not available.", _asa_scenarios.error_msg
"Please wait, the scenario index is still downloading.", _asa_scenarios.error_msg
)
return jsonify( [
make_entry( scenario )
@ -110,13 +121,13 @@ def get_roar_scenario_index():
with _roar_scenarios:
if _roar_scenarios.index is None:
return _make_not_available_response(
"The ROAR scenarios are not available.", _roar_scenarios.error_msg
"Please wait, the ROAR scenarios are still downloading.", _roar_scenarios.error_msg
)
return jsonify( _roar_scenarios.index )
def _make_not_available_response( msg, msg2 ):
"""Generate a "not available" response."""
resp = { "error": msg }
resp = { "warning": msg }
if msg2:
resp[ "message" ] = msg2
return jsonify( resp )
@ -124,7 +135,7 @@ def _make_not_available_response( msg, msg2 ):
# ---------------------------------------------------------------------
@app.route( "/scenario/<scenario_id>" )
def get_scenario( scenario_id ):
def get_scenario( scenario_id ): #pylint: disable=too-many-locals
"""Return a scenario."""
# get the parameters
@ -145,6 +156,43 @@ def get_scenario( scenario_id ):
score = 100 * nWins / nGames
return int( score + 0.5 )
# get any files available for download
downloads = {}
keys = { "vt_setup": "vaslTemplates", "vasl_setup": "vaslTemplateSetups", "screenshot": "templateImages" }
for key in keys:
for entry in scenario.get( keys[key], [] ):
fname = os.path.basename( entry.get( "url", "" ) )
pos = fname.find( "|" )
if pos < 0:
continue
fkey = fname[:pos]
if fkey not in downloads:
downloads[ fkey ] = {
"user": entry.get( "user" ),
"timestamp": entry.get( "created" ),
}
downloads[ fkey ][ key ] = entry.get( "url" )
downloads = sorted( downloads.values(), key=lambda u: u["timestamp"], reverse=True )
if downloads:
args[ "downloads" ] = [
d for d in downloads
if "vt_setup" in d or "vasl_setup" in d
]
# get the map previews
map_images = []
for fgroup in downloads:
if "screenshot" in fgroup:
map_images.append( fgroup )
for map_image in scenario.get( "mapImages", [] ):
map_images.append( {
"screenshot": map_image.get( "url" ),
"user": map_image.get( "user" ),
"timestamp": map_image.get( "created" ),
} )
if map_images:
args[ "map_images" ] = map_images
# get the ASL Scenario Archive playings
playings = scenario.get( "playings", [ {} ] )[ 0 ]
nGames = parse_int( playings.get( "totalGames" ), 0 )
@ -282,7 +330,6 @@ def get_scenario_card( scenario_id ): #pylint: disable=too-many-branches
args[ "DEFENDER_NAME" ] = scenario.get( "defender" )
args[ "ATTACKER_NAME" ] = scenario.get( "attacker" )
args[ "BOARDS" ] = ", ".join( str(m) for m in scenario.get("maps",[]) )
args[ "MAP_IMAGES" ] = scenario.get( "mapImages" )
overlays = ", ".join( str(o) for o in scenario.get("overlays",[]) )
if overlays.upper() == "NONE":
overlays = None
@ -368,6 +415,197 @@ def _make_scenario_name( scenario ):
# ---------------------------------------------------------------------
@app.route( "/prepare-asa-upload", methods=["POST"] )
def prepare_asa_upload(): #pylint: disable=too-many-locals
"""Prepare files for upload to the ASL Scenario Archive."""
# parse the request
vsav_data = request.json[ "vsav_data" ]
vsav_filename = request.json[ "filename" ]
# initialize
start_time = time.time()
logger = logging.getLogger( "prepare_asa_upload" )
try:
# get the VSAV data (we do this inside the try block so that the user gets shown
# a proper error dialog if there's a problem decoding the base64 data)
vsav_data = base64.b64decode( vsav_data )
logger.info( "Preparing VSAV (#bytes=%d): %s", len(vsav_data), vsav_filename )
with TempFile() as input_file:
# save the VSAV data in a temp file
input_file.write( vsav_data )
input_file.close( delete=False )
fname = app.config.get( "PREPARE_ASA_UPLOAD_INPUT" ) # nb: for diagnosing problems
if fname:
logger.debug( "Saving a copy of the VSAV data: %s", fname )
with open( fname, "wb" ) as fp:
fp.write( vsav_data )
# prepare the files to be uploaded
with TempFile() as stripped_vsav_file, TempFile() as screenshot_file:
# run the VASSAL shim to prepare the VSAV file
stripped_vsav_file.close( delete=False )
screenshot_file.close( delete=False )
vassal_shim = VassalShim()
vassal_shim.prepare_asa_upload(
input_file.name, stripped_vsav_file.name, screenshot_file.name
)
# read the stripped VSAV data
with open( stripped_vsav_file.name, "rb" ) as fp:
stripped_vsav = fp.read()
stripped_vsav_file.save_copy(
app.config.get( "PREPARE_ASA_UPLOAD_STRIPPED_VSAV" ),
logger, "stripped VSAV"
)
# read the screenshot image
screenshot_file.save_copy(
app.config.get( "PREPARE_ASA_UPLOAD_SCREENSHOT" ),
logger, "generated screenshot"
)
if os.path.getsize( screenshot_file.name ) == 0:
# NOTE: The VASSAL shim sometimes crashes while trying to generate a screenshot :-(
screenshot_data = None
else:
# NOTE: These screenshots are used as map preview images on the ASL Scenario Archive
# web site (and by us, as well), so we want to optimize them for size.
# NOTE: I tried changing the PNG from 24-bit RGB to using a palette:
# img.convert( "P", palette=Image.ADAPTIVE/WEB )
# but since PNG is a lossless format, the benefits are minimal. Also, weird things happen
# if we do this before shrinking the image, which makes calculating the ratio tricky.
# clean up the original screenshot
img = trim_image( screenshot_file.name )
img = remove_alpha_from_image( img )
# get the image data
def save_image( img ): #pylint: disable=missing-docstring
quality = parse_int( app.config.get( "ASA_SCREENSHOT_QUALITY" ), 50 )
return get_image_data( img, format="JPEG", quality=quality, optimize=True, subsampling=0 )
screenshot_data = save_image( img )
# resize it to (roughly) the maximum allowed size
max_size = parse_int( app.config.get( "ASA_MAX_SCREENSHOT_SIZE" ), 200 ) * 1024
if len(screenshot_data) > max_size:
ratio = math.sqrt( float(max_size) / len(screenshot_data) )
img = img.resize( ( int(img.width * ratio), int(img.height * ratio) ), Image.ANTIALIAS )
# add a border
border_size = parse_int( app.config.get( "ASA_SCREENSHOT_BORDER_SIZE" ), 5 )
img = ImageOps.expand( img, border_size, (255,255,255,255) )
# get the final image data
screenshot_data = save_image( img )
except Exception as ex: #pylint: disable=broad-except
return VassalShim.translate_vassal_shim_exception( ex, logger )
# return the results
logger.info( "Prepared the VSAV file OK: elapsed=%.3fs", time.time()-start_time )
results = {
"filename": vsav_filename,
"stripped_vsav": base64.b64encode( stripped_vsav ).decode( "utf-8" ),
}
if screenshot_data:
results[ "screenshot" ] = base64.b64encode( screenshot_data ).decode( "utf-8" )
return jsonify( results )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@app.route( "/on-successful-asa-upload/<scenario_id>" )
def on_successful_asa_upload( scenario_id ):
"""Update the local scenario index after a successful upload."""
# download the specified scenario
url = app.config["ASA_GET_SCENARIO_URL"].replace( "{ID}", scenario_id )
try:
fp = urllib.request.urlopen( url )
new_scenario = json.loads( fp.read().decode( "utf-8" ) )
except Exception as ex: #pylint: disable=broad-except
msg = str( getattr(ex,"reason",None) or ex )
return jsonify( { "status": "error", "message": msg } )
# update the local index
if os.path.isfile( _asa_scenarios.cache_fname ) and not app.config.get( "DISABLE_LOCAL_ASA_INDEX_UPDATES" ):
# NOTE: Since the downloaded index file contains a *list* of scenarios, we append the new scenario info
# to the end of that list, and then the next time the program starts and reads the list into a dict,
# the most-recent version is the one that will ultimately be used. This lets us identify these temporary changes
# that have been made to the cached index file (which will be overwritten when we download a fresh copy).
# This is not particularly efficient, but it won't happen too often.
with open( _asa_scenarios.cache_fname, "r" ) as fp:
data = json.load( fp )
data["scenarios"].append( new_scenario )
with open( _asa_scenarios.cache_fname, "w" ) as fp:
json.dump( data, fp )
# update the in-memory scenario index
with _asa_scenarios:
_asa_scenarios.index[ scenario_id ] = new_scenario
return jsonify( { "status": "ok" } )
# ---------------------------------------------------------------------
_last_asa_upload = None
@app.route( "/test-asa-upload/<int:scenario_id>", methods=["POST"] )
def test_asa_upload( scenario_id ):
"""A test endpoint that can be used to simulate ASL Scenario Archive uploads."""
def save_file( key, asa_upload, convert=None ):
"""Save a file that has been uploaded to us."""
f = request.files.get( key )
if not f:
print( "- {}: not present.".format( key ) )
return
data = f.read()
asa_upload[ key ] = convert(data) if convert else data
print( "- {}: {} ({}) ; #bytes={}".format( key, f.filename, f.content_type, len(data) ) )
fname = app.config.get( "SAVE_ASA_UPLOAD_" + key.upper() )
if fname:
with open( fname, "wb" ) as fp:
fp.write( data )
print( " - Saved to:", fname )
def make_response( fname ):
"""Generate a response."""
dname = os.path.join( os.path.dirname(__file__), "tests/fixtures/asa-responses/" )
fname = os.path.join( dname, "{}.json".format( fname ) )
resp = json.load( open( fname, "r" ) )
return jsonify( resp )
# simulate a slow response
delay = parse_int( app.config.get( "ASA_UPLOAD_DELAY" ), 0 )
if delay > 0:
time.sleep( delay )
# parse the request
user_name = request.args.get( "user" )
if not user_name:
return make_response( "missing-user-name" )
api_token = request.args.get( "token" )
if not api_token:
return make_response( "missing-token" )
if api_token == "incorrect-token":
return make_response( "incorrect-token" )
# process the request
print( "ASA upload: id={} ; user=\"{}\" ; token=\"{}\"".format( scenario_id, user_name,api_token ) )
asa_upload = {}
save_file( "vasl_setup", asa_upload )
save_file( "vt_setup", asa_upload, lambda v: json.loads(v) ) #pylint: disable=unnecessary-lambda
save_file( "screenshot", asa_upload )
if asa_upload:
asa_upload.update( { "user": user_name, "token": api_token } )
global _last_asa_upload
_last_asa_upload = asa_upload
return make_response( "ok" )
# ---------------------------------------------------------------------
@app.route( "/scenario/nat-report" )
def scenario_nat_report():
"""Generate the scenario nationalities report (for testing porpoises)."""
@ -402,4 +640,7 @@ def _split_date_parts( val ):
return None
if mo.group(1) == "1901":
return None # nb: 1901-01-01 seems to be used as a "invalid date" marker
return [ int(mo.group(3)), int(mo.group(2)), int(mo.group(1)) ]
parts = [ int(mo.group(3)), int(mo.group(2)), int(mo.group(1)) ]
if parts == [ 0, 0, 0, ]:
return None # nb: 0000-00-00 also seems to be used as an "invalid date" marker
return parts

@ -68,20 +68,18 @@
.scenario-card .balance-graph .progressbar .score { position: absolute ; font-size: 70% ; font-style: italic ; color: #666 ; left: 35% ; }
/* scenario card - misc */
.scenario-card a.map-preview img { height: 0.75em ; margin-left: 0.25em ; }
.scenario-card a.map-preview:focus { outline: 0 ; }
.scenario-card .boards img.map-previews { height: 0.75em ; margin-left: 0.5em ; cursor: pointer ; }
.scenario-card .boards .map-preview-count { font-size: 80% ; font-style: italic ; color: #888 ; }
.scenario-card .errata ul { margin-top: 0 ; }
.scenario-card .errata .source { font-size: 90% ; font-style: italic ; color: #666 ; }
/* scenario info dialog */
.ui-dialog.scenario-info { border-radius: 10px ; }
.ui-dialog.scenario-info .ui-dialog-titlebar { display: none ; }
.ui-dialog.scenario-info .credit { float: left ; margin-right: 0.5em ; display: flex ; align-items: center ; }
.ui-dialog.scenario-info .credit img { height: 1.4em ; margin-right: 0.5em ; opacity: 0.7 ; }
.ui-dialog.scenario-info .credit .caption { font-size: 70% ; line-height: 1em ; margin-top: -4px ; }
.ui-dialog.scenario-info .credit a { text-decoration: none ; font-style: italic ; color: #666 ; }
.ui-dialog.scenario-info .credit a:focus { outline: 0 ; }
#scenario-info-dialog .scenario-card { height: 100% ; overflow-y: hidden ; }
#scenario-info-dialog .connect-roar { display: inline-block ; margin-top: 0.25em ; font-size: 80% ; color: #444 ; cursor: pointer ; }
#scenario-info-dialog .connect-roar img { height: 0.75em ; Xmargin-right: 0.25em ; opacity: 0.7 ; }
#scenario-info-dialog .disconnect-roar img { height: 0.5em ; margin-left: 0.5em ; cursor: pointer ; }
/* lightgallery */
.lg-backdrop.in { opacity: 0.85 ; }

@ -0,0 +1,20 @@
.ui-dialog.scenario-downloads { border-radius: 5px ; }
.ui-dialog.scenario-downloads .ui-dialog-titlebar { background: none ; border: none ; cursor: auto ; }
.ui-dialog.scenario-downloads .ui-dialog-titlebar-close { display: none; }
#scenario-downloads-dialog { padding-top: 0 !important ; }
#scenario-downloads-dialog .fgroups { margin: 0 ; }
#scenario-downloads-dialog .fgroup {
border: 1px solid #ccc ; border-radius: 5px ;
margin-bottom: 0.5em ;
padding: 0.5em ;
display: flex ;
list-style-type: none ;
}
#scenario-downloads-dialog .fgroup:last-of-type { margin-bottom: 0 ; }
#scenario-downloads-dialog .fgroup .screenshot { width: 5em ; max-height: 4em ; margin-right: 1em ; text-align: center ; }
#scenario-downloads-dialog .fgroup .screenshot img { max-width: 100% ; max-height: 4em ; }
#scenario-downloads-dialog .fgroup .user { font-style: italic ; }
#scenario-downloads-dialog .fgroup .timestamp { font-size: 80% ; font-style: italic ; color: #888 ; }
#scenario-downloads-dialog .fgroup button { float: left ; margin: 0.5em 0.5em 0 0 ; padding: 2px 5px ; display: flex ; align-items: center ; font-size: 80% ; }
#scenario-downloads-dialog .fgroup button img { height: 1em ; margin-right: 0.5em ; }

@ -55,6 +55,8 @@
#scenario-search .import-control .buttons button { float: right ; margin-left: 0.5em ; padding: 0.1em 0.5em ; }
#scenario-search .import-control .buttons button.import { height: 2em ; display: flex ; align-items: center ; }
#scenario-search .import-control .buttons button.import img { height: 1em ; margin-right: 0.5em ; }
#scenario-search .import-control .buttons button.downloads { height: 2em ; display: flex ; align-items: center ; }
#scenario-search .import-control .buttons button.downloads img { height: 1em ; margin-right: 0.5em ; }
#scenario-search .import-control .buttons button.ok { background: #ddd ; }
#scenario-search .import-control .buttons button.ok:hover { background: #ccc ; }
#scenario-search .import-control .warnings { margin-bottom: 0.5em ; padding: 0.25em 0 0 10px ; font-size: 90% ; }

@ -0,0 +1,56 @@
.ui-dialog.scenario-upload .ui-dialog-titlebar { background: #e0e0a0 ; }
#scenario-upload-dialog { display: flex ; flex-direction: column ; padding: 1em !important ; overflow-y: hidden ; }
#scenario-upload-dialog label { display: inline-block ; width: 5.75em ; font-weight: bold ; }
#scenario-upload-dialog .row { margin-bottom: 2px ; }
#scenario-upload-dialog .hint { font-style: italic ; color: #888 ; }
#scenario-upload-dialog .row .hint { margin-left: 0.5em ; font-size: 80% ; font-style: italic ; color: #666 ; }
#scenario-upload-dialog .row .hint a { color: #666 ; }
#scenario-upload-dialog .scenario-id { font-size: 80% ; font-style: italic ; color: #666 ; }
#scenario-upload-dialog .asa-id { font-size: 70% ; font-style: italic ; color: #666 ; }
#scenario-upload-dialog .auth { margin: 0.25em 0 1em 0 ; }
#scenario-upload-dialog .disclaimer { margin-top: 1em ; font-size: 80% ; font-style: italic ; color: #444 ; }
#scenario-upload-dialog img.remove { height: 12px ; position: absolute ; top: 5px ; right: 5px ; z-index: 5 ; cursor: pointer ; }
#scenario-upload-dialog .grid { flex: 1 ; display: flex ; }
#scenario-upload-dialog .grid .left { flex: 4 ; margin-right: 1em ; }
#scenario-upload-dialog .grid .right { flex: 6 ; }
#scenario-upload-dialog .vsav-container {
position: relative ;
padding: 1em ;
border: 1px solid #888 ; border-radius: 5px ;
background: #f8f8f8 ;
}
#scenario-upload-dialog .vsav-container .hint { display: flex ; }
#scenario-upload-dialog .vsav-container .hint img {
margin-right: 0.5em ; height: 2em ; opacity: 0.7 ;
}
#scenario-upload-dialog .vsav-container .file-info { display: flex ; }
#scenario-upload-dialog .vsav-container .file-info img {
margin-right: 0.5em ; height: 2em ; opacity: 0.8 ;
}
#scenario-upload-dialog .vsav-container .file-info .name { font-size: 80% ; font-family: monospace ; color: #444 ; }
#scenario-upload-dialog .screenshot-container {
display: flex ; flex-direction: column ; justify-content: center ;
height: 100% ;
border: 1px solid #888 ; border-radius: 5px ;
background: #f8f8f8 ;
position: relative ;
}
#scenario-upload-dialog .screenshot-container .hint { display: flex ; justify-content: center ; align-items: center ; }
#scenario-upload-dialog .screenshot-container .hint img {
float: left ; margin-right: 0.5em ; height: 2.5em ; opacity: 0.6 ;
}
#scenario-upload-dialog .screenshot-container .preview {
display: flex ; flex-direction: column ; align-items: center ; justify-content: center ;
height: calc( 100% - 10px ) ;
margin: 5px ;
}
#scenario-upload-dialog .screenshot-container .preview img {
object-fit: scale-down ; max-width: 100% ; max-height: 100% ;
border: 1px dotted #aaa ;
}

@ -1,48 +0,0 @@
div.jquery-image-zoom {
line-height: 0;
font-size: 0;
z-index: 1000;
border: 5px solid #fff;
background: #eee; /* TM 25jan15: Added this to make it easier to see images with transparent backgrounds. */
margin: -5px;
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
div.jquery-image-zoom a {
background: url(/static/imageZoom/jquery.imageZoom.png) no-repeat;
display: block;
width: 25px;
height: 25px;
position: absolute;
left: -17px;
top: -17px;
/* IE-users are prolly used to close-link in right-hand corner */
*left: auto;
*right: -17px;
text-decoration: none;
text-indent: -100000px;
outline: 0;
z-index: 11;
}
div.jquery-image-zoom a:hover {
background-position: left -25px;
}
div.jquery-image-zoom img,
div.jquery-image-zoom embed,
div.jquery-image-zoom object,
div.jquery-image-zoom div {
width: 100%;
height: 100%;
margin: 0;
}

@ -1 +0,0 @@
jQuery.fn.imageZoom=function(c,b){var a=c.extend({speed:200,dontFadeIn:1,hideClicked:1,imageMargin:30,className:"jquery-image-zoom",loading:"Loading..."},b);a.doubleSpeed=a.speed/4;c(document).keydown(function(d){if(d.keyCode==27){c("div.jquery-image-zoom a").click()}});return this.click(function(k){var h=c(k.target);var g=h.is("a")?h:h.parents("a");g=(g&&g.is("a")&&g.attr("href").search(/(.*)\.(jpg|jpeg|gif|png|bmp|tif|tiff)$/gi)!=-1)?g:false;var i=(g&&g.find("img").length)?g.find("img"):false;c("div.jquery-image-zoom a").click();if(g){g.oldText=g.text();g.setLoadingImg=function(){if(i){i.css({opacity:"0.5"})}else{g.text(a.loading)}};g.setNotLoadingImg=function(){if(i){i.css({opacity:"1"})}else{g.text(g.oldText)}};var d=g.attr("href");if(c("div."+a.className+' img[src="'+d+'"]').length){return false}var j=function(l){g.setNotLoadingImg();var u=i?i:g;var q=i?a.hideClicked:0;var p=u.offset();var n={width:u.outerWidth(),height:u.outerHeight(),left:p.left,top:p.top};var o=c('<div><img src="'+d+'" alt=""/></div>').css("position","absolute").appendTo(document.body);var m={width:l.width,height:l.height};var s={width:c(window).width(),height:c(window).height()};if(m.width>(s.width-a.imageMargin*2)){var r=s.width-a.imageMargin*2;m.height=(r/m.width)*m.height;m.width=r}if(m.height>(s.height-a.imageMargin*2)){var t=s.height-a.imageMargin*2;m.width=(t/m.height)*m.width;m.height=t}m.left=(s.width-m.width)/2+c(window).scrollLeft();m.top=(s.height-m.height)/2+c(window).scrollTop();var e=c('<a href="#">Close</a>').appendTo(o).hide();if(q){g.css("visibility","hidden")}o.addClass(a.className).css(n).animate(m,a.speed,function(){e.fadeIn(a.doubleSpeed)});var v=function(){e.fadeOut(a.doubleSpeed,function(){o.animate(n,a.speed,function(){g.css("visibility","visible");o.remove()})});return false};o.click(v);e.click(v)};var f=new Image();f.src=d;if(f.complete){j(f)}else{g.setLoadingImg();f.onload=function(){j(f)}}return false}})};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@ -160,9 +160,7 @@ function addFilesToUploadList( files )
var currFile = files[ currFileNo ] ;
fileReader.onload = function() {
// get the file data
vlog_data = fileReader.result ;
if ( vlog_data.substring(0,5) === "data:" )
vlog_data = vlog_data.split( "," )[1] ;
vlog_data = removeBase64Prefix( fileReader.result ) ;
// add the file to the list
addFileToUploadList( currFile.name, vlog_data ) ;
// read the next file

File diff suppressed because one or more lines are too long

@ -0,0 +1,51 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
<json>
<![CDATA[
{
"fontFamily": "lg",
"majorVersion": 1,
"minorVersion": 0,
"fontURL": "https://github.com/sachinchoolur/lightGallery",
"copyright": "sachin",
"license": "MLT",
"licenseURL": "http://opensource.org/licenses/MIT",
"description": "Font generated by IcoMoon.",
"version": "Version 1.0",
"fontId": "lg",
"psName": "lg",
"subFamily": "Regular",
"fullName": "lg"
}
]]>
</json>
</metadata>
<defs>
<font id="lg" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe01a;" glyph-name="pause_circle_outline" data-tags="pause_circle_outline" d="M554 256.667v340h86v-340h-86zM512 84.667q140 0 241 101t101 241-101 241-241 101-241-101-101-241 101-241 241-101zM512 852.667q176 0 301-125t125-301-125-301-301-125-301 125-125 301 125 301 301 125zM384 256.667v340h86v-340h-86z" />
<glyph unicode="&#xe01d;" glyph-name="play_circle_outline" data-tags="play_circle_outline" d="M512 84.667q140 0 241 101t101 241-101 241-241 101-241-101-101-241 101-241 241-101zM512 852.667q176 0 301-125t125-301-125-301-301-125-301 125-125 301 125 301 301 125zM426 234.667v384l256-192z" />
<glyph unicode="&#xe033;" glyph-name="stack-2" data-tags="stack-2" d="M384 853.334h426.667q53 0 90.5-37.5t37.5-90.5v-426.667q0-53-37.5-90.5t-90.5-37.5h-426.667q-53 0-90.5 37.5t-37.5 90.5v426.667q0 53 37.5 90.5t90.5 37.5zM170.667 675.334v-547.333q0-17.667 12.5-30.167t30.167-12.5h547.333q-13.333-37.667-46.333-61.5t-74.333-23.833h-426.667q-53 0-90.5 37.5t-37.5 90.5v426.667q0 41.333 23.833 74.333t61.5 46.333zM810.667 768h-426.667q-17.667 0-30.167-12.5t-12.5-30.167v-426.667q0-17.667 12.5-30.167t30.167-12.5h426.667q17.667 0 30.167 12.5t12.5 30.167v426.667q0 17.667-12.5 30.167t-30.167 12.5z" />
<glyph unicode="&#xe070;" glyph-name="clear" data-tags="clear" d="M810 664.667l-238-238 238-238-60-60-238 238-238-238-60 60 238 238-238 238 60 60 238-238 238 238z" />
<glyph unicode="&#xe094;" glyph-name="arrow-left" data-tags="arrow-left" d="M426.667 768q17.667 0 30.167-12.5t12.5-30.167q0-18-12.667-30.333l-225.667-225.667h665q17.667 0 30.167-12.5t12.5-30.167-12.5-30.167-30.167-12.5h-665l225.667-225.667q12.667-12.333 12.667-30.333 0-17.667-12.5-30.167t-30.167-12.5q-18 0-30.333 12.333l-298.667 298.667q-12.333 13-12.333 30.333t12.333 30.333l298.667 298.667q12.667 12.333 30.333 12.333z" />
<glyph unicode="&#xe095;" glyph-name="arrow-right" data-tags="arrow-right" d="M597.333 768q18 0 30.333-12.333l298.667-298.667q12.333-12.333 12.333-30.333t-12.333-30.333l-298.667-298.667q-12.333-12.333-30.333-12.333-18.333 0-30.5 12.167t-12.167 30.5q0 18 12.333 30.333l226 225.667h-665q-17.667 0-30.167 12.5t-12.5 30.167 12.5 30.167 30.167 12.5h665l-226 225.667q-12.333 12.333-12.333 30.333 0 18.333 12.167 30.5t30.5 12.167z" />
<glyph unicode="&#xe0f2;" glyph-name="vertical_align_bottom" data-tags="vertical_align_bottom" d="M170 128.667h684v-86h-684v86zM682 384.667l-170-172-170 172h128v426h84v-426h128z" />
<glyph unicode="&#xe1ff;" glyph-name="apps" data-tags="apps" d="M682 84.667v172h172v-172h-172zM682 340.667v172h172v-172h-172zM426 596.667v172h172v-172h-172zM682 768.667h172v-172h-172v172zM426 340.667v172h172v-172h-172zM170 340.667v172h172v-172h-172zM170 84.667v172h172v-172h-172zM426 84.667v172h172v-172h-172zM170 596.667v172h172v-172h-172z" />
<glyph unicode="&#xe20c;" glyph-name="fullscreen" data-tags="fullscreen" d="M598 724.667h212v-212h-84v128h-128v84zM726 212.667v128h84v-212h-212v84h128zM214 512.667v212h212v-84h-128v-128h-84zM298 340.667v-128h128v-84h-212v212h84z" />
<glyph unicode="&#xe20d;" glyph-name="fullscreen_exit" data-tags="fullscreen_exit" d="M682 596.667h128v-84h-212v212h84v-128zM598 128.667v212h212v-84h-128v-128h-84zM342 596.667v128h84v-212h-212v84h128zM214 256.667v84h212v-212h-84v128h-128z" />
<glyph unicode="&#xe311;" glyph-name="zoom_in" data-tags="zoom_in" d="M512 512.667h-86v-86h-42v86h-86v42h86v86h42v-86h86v-42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-104-66-180l12-12h34z" />
<glyph unicode="&#xe312;" glyph-name="zoom_out" data-tags="zoom_out" d="M298 554.667h214v-42h-214v42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-104-66-180l12-12h34z" />
<glyph unicode="&#xe80d;" glyph-name="share" data-tags="share" d="M768 252.667c68 0 124-56 124-124s-56-126-124-126-124 58-124 126c0 10 0 20 2 28l-302 176c-24-22-54-34-88-34-70 0-128 58-128 128s58 128 128 128c34 0 64-12 88-34l300 174c-2 10-4 20-4 30 0 70 58 128 128 128s128-58 128-128-58-128-128-128c-34 0-64 14-88 36l-300-176c2-10 4-20 4-30s-2-20-4-30l304-176c22 20 52 32 84 32z" />
<glyph unicode="&#xe900;" glyph-name="rotate_left" data-tags="rotate_left" d="M554 764.667q126-16 213-112t87-226-87-226-213-112v86q92 16 153 87t61 165-61 165-153 87v-166l-194 190 194 194v-132zM302 156.667l62 62q46-34 106-44v-86q-96 12-168 68zM260 384.667q10-58 42-106l-60-60q-56 74-68 166h86zM304 574.667q-36-52-44-106h-86q12 90 70 166z" />
<glyph unicode="&#xe901;" glyph-name="rotate_right" data-tags="rotate_right" d="M720 278.667q34 46 44 106h86q-12-92-68-166zM554 174.667q60 10 106 44l62-62q-72-56-168-68v86zM850 468.667h-86q-10 60-44 106l62 60q58-72 68-166zM664 702.667l-194-190v166q-92-16-153-87t-61-165 61-165 153-87v-86q-126 16-213 112t-87 226 87 226 213 112v132z" />
<glyph unicode="&#xe902;" glyph-name="swap_horiz" data-tags="swap_horiz" d="M896 554.667l-170-170v128h-300v84h300v128zM298 468.667v-128h300v-84h-300v-128l-170 170z" />
<glyph unicode="&#xe903;" glyph-name="swap_vert" data-tags="swap_vert" d="M384 810.667l170-170h-128v-300h-84v300h-128zM682 212.667h128l-170-170-170 170h128v300h84v-300z" />
<glyph unicode="&#xe904;" glyph-name="facebook-with-circle" data-tags="facebook-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM628.429 612.659h-73.882c-8.755 0-18.483-11.52-18.483-26.829v-53.35h92.416l-13.978-76.083h-78.438v-228.403h-87.194v228.403h-79.104v76.083h79.104v44.749c0 64.205 44.544 116.378 105.677 116.378h73.882v-80.947z" />
<glyph unicode="&#xe905;" glyph-name="google-with-circle" data-tags="google+-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM483.686 249.805c-30.874-15.002-64.102-16.589-76.954-16.589-2.458 0-3.84 0-3.84 0s-1.178 0-2.765 0c-20.070 0-119.962 4.608-119.962 95.59 0 89.395 108.8 96.41 142.131 96.41h0.87c-19.251 25.702-15.258 51.61-15.258 51.61-1.69-0.102-4.147-0.205-7.168-0.205-12.544 0-36.762 1.997-57.549 15.411-25.498 16.384-38.4 44.288-38.4 82.893 0 109.107 119.142 113.51 120.32 113.613h118.989v-2.611c0-13.312-23.91-15.923-40.192-18.125-5.53-0.819-16.64-1.894-19.763-3.482 30.157-16.128 35.021-41.421 35.021-79.104 0-42.906-16.794-65.587-34.611-81.51-11.059-9.882-19.712-17.613-19.712-28.006 0-10.189 11.878-20.582 25.702-32.717 22.579-19.917 53.555-47.002 53.555-92.723 0-47.258-20.326-81.050-60.416-100.454zM742.4 460.8h-76.8v-76.8h-51.2v76.8h-76.8v51.2h76.8v76.8h51.2v-76.8h76.8v-51.2zM421.018 401.92c-2.662 0-5.325-0.102-8.038-0.307-22.733-1.69-43.725-10.189-58.88-24.013-15.053-13.619-22.733-30.822-21.658-48.179 2.304-36.403 41.37-57.702 88.832-54.323 46.694 3.379 77.824 30.31 75.571 66.714-2.15 34.202-31.898 60.109-75.827 60.109zM465.766 599.808c-12.39 43.52-32.358 56.422-63.386 56.422-3.328 0-6.707-0.512-9.933-1.382-13.466-3.84-24.166-15.053-30.106-31.744-6.093-16.896-6.451-34.509-1.229-54.579 9.472-35.891 34.97-61.901 60.672-61.901 3.379 0 6.758 0.41 9.933 1.382 28.109 7.885 45.722 50.79 34.048 91.802z" />
<glyph unicode="&#xe906;" glyph-name="pinterest-with-circle" data-tags="pinterest-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM545.638 344.32c-31.539 2.406-44.749 18.022-69.427 32.973-13.568-71.219-30.157-139.52-79.309-175.206-15.206 107.725 22.221 188.518 39.629 274.381-29.645 49.92 3.533 150.323 66.099 125.645 76.954-30.515-66.662-185.6 29.747-205.005 100.659-20.173 141.773 174.694 79.36 237.978-90.214 91.494-262.502 2.099-241.306-128.87 5.12-32 38.246-41.728 13.21-85.914-57.702 12.8-74.957 58.317-72.704 118.989 3.533 99.328 89.242 168.909 175.155 178.483 108.698 12.083 210.688-39.885 224.819-142.182 15.821-115.405-49.101-240.282-165.274-231.27z" />
<glyph unicode="&#xe907;" glyph-name="twitter-with-circle" data-tags="twitter-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM711.936 549.683c0.205-4.198 0.256-8.397 0.256-12.493 0-128-97.331-275.507-275.405-275.507-54.682 0-105.574 15.974-148.378 43.52 7.526-0.922 15.258-1.28 23.091-1.28 45.363 0 87.091 15.411 120.218 41.421-42.342 0.819-78.080 28.774-90.419 67.174 5.888-1.075 11.93-1.69 18.176-1.69 8.806 0 17.408 1.178 25.498 3.379-44.288 8.909-77.67 48.026-77.67 94.925v1.178c13.056-7.219 28.006-11.622 43.878-12.134-26.010 17.408-43.059 47.002-43.059 80.64 0 17.715 4.762 34.406 13.107 48.691 47.77-58.573 119.040-97.075 199.526-101.222-1.69 7.117-2.509 14.49-2.509 22.118 0 53.402 43.315 96.819 96.819 96.819 27.802 0 52.992-11.776 70.656-30.618 22.067 4.403 42.752 12.39 61.44 23.501-7.219-22.579-22.528-41.574-42.547-53.606 19.61 2.406 38.246 7.578 55.603 15.309-12.954-19.405-29.389-36.506-48.282-50.125z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

@ -0,0 +1,4 @@
/*! lg-rotate - v1.2.0 - 2020-09-19
* http://sachinchoolur.github.io/lightGallery
* Copyright (c) 2020 Sachin N; Licensed GPLv3 */
!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){!function(){"use strict";var b={rotate:!0,rotateLeft:!0,rotateRight:!0,flipHorizontal:!0,flipVertical:!0},c=function(c){return this.core=a(c).data("lightGallery"),this.core.s=a.extend({},b,this.core.s),this.core.s.rotate&&this.core.doCss()&&this.init(),this};c.prototype.buildTemplates=function(){var a="";this.core.s.flipVertical&&(a+='<button aria-label="Flip vertical" class="lg-flip-ver lg-icon"></button>'),this.core.s.flipHorizontal&&(a+='<button aria-label="flip horizontal" class="lg-flip-hor lg-icon"></button>'),this.core.s.rotateLeft&&(a+='<button aria-label="Rotate left" class="lg-rotate-left lg-icon"></button>'),this.core.s.rotateRight&&(a+='<button aria-label="Rotate right" class="lg-rotate-right lg-icon"></button>'),this.core.$outer.find(".lg-toolbar").append(a)},c.prototype.init=function(){var a=this;this.buildTemplates(),this.rotateValuesList={},this.core.$el.on("onAferAppendSlide.lg.tm.rotate",function(b,c){a.core.$slide.eq(c).find(".lg-img-wrap").wrap('<div class="lg-img-rotate"></div>')}),this.core.$outer.find(".lg-rotate-left").on("click.lg",this.rotateLeft.bind(this)),this.core.$outer.find(".lg-rotate-right").on("click.lg",this.rotateRight.bind(this)),this.core.$outer.find(".lg-flip-hor").on("click.lg",this.flipHorizontal.bind(this)),this.core.$outer.find(".lg-flip-ver").on("click.lg",this.flipVertical.bind(this)),this.core.$el.on("onBeforeSlide.lg.tm.rotate",function(b,c,d){a.rotateValuesList[d]||(a.rotateValuesList[d]={rotate:0,flipHorizontal:1,flipVertical:1})})},c.prototype.applyStyles=function(){this.core.$slide.eq(this.core.index).find(".lg-img-rotate").css("transform","rotate("+this.rotateValuesList[this.core.index].rotate+"deg) scale3d("+this.rotateValuesList[this.core.index].flipVertical+", "+this.rotateValuesList[this.core.index].flipHorizontal+", 1)")},c.prototype.rotateLeft=function(){this.rotateValuesList[this.core.index].rotate-=90,this.applyStyles()},c.prototype.rotateRight=function(){this.rotateValuesList[this.core.index].rotate+=90,this.applyStyles()},c.prototype.flipHorizontal=function(){this.rotateValuesList[this.core.index].flipVertical*=-1,this.applyStyles()},c.prototype.flipVertical=function(){this.rotateValuesList[this.core.index].flipHorizontal*=-1,this.applyStyles()},c.prototype.destroy=function(){this.core.$el.off(".lg.tm.rotate"),this.rotateValuesList={}},a.fn.lightGallery.modules.rotate=c}()});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -174,11 +174,11 @@ function getRoarScenarioIndex( onReady )
// nope - download it
$.getJSON( gGetRoarScenarioIndexUrl, function( resp ) {
if ( resp.error ) {
var msg = resp.error ;
if ( resp.warning ) {
var msg = resp.warning ;
if ( resp.message )
msg += "<div class='pre'>" + escapeHTML(resp.message) + "</div>" ;
showErrorMsg( msg ) ;
showWarningMsg( msg ) ;
return ;
}
_roarScenarioIndex = resp ;

@ -0,0 +1,454 @@
/* jshint esnext: true */
( function() { // nb: put the entire file into its own local namespace, global stuff gets added to window.
var gVsavData, gScreenshotData ;
var $gDialog, $gVsavContainer, $gScreenshotContainer ;
// --------------------------------------------------------------------
window.uploadScenario = function() {
// initialize
var asaScenarioId = $( "input[name='ASA_ID']" ).val() ;
function onAddVsavFile() {
if ( ! canAddVsavFile() )
return ;
if ( getUrlParam( "vsav_persistence" ) ) {
// FOR TESTING PORPOISES! We can't control a file upload from Selenium (since
// the browser will use native controls), so we get the data from a <textarea>).
var $elem = $( "#_vsav-persistence_" ) ;
var vsavData = $elem.val() ;
$elem.val( "" ) ; // nb: let the test suite know we've received the data
doSelectVsavData( "test.vsav", vsavData ) ;
return ;
}
$( "#select-vsav-for-upload" ).trigger( "click" ) ; // nb: onSelectVsavFile() will be called
}
function canAddVsavFile() {
return $gVsavContainer.find( ".hint" ).css( "display" ) !== "none" ;
}
function onSelectVsavFile( file ) {
// read the selected file
var fileReader = new FileReader() ;
fileReader.onload = function() {
var vsavData = fileReader.result ;
var fname = file.name ;
// check the file size
var maxBytes = gAppConfig.ASA_MAX_VASL_SETUP_SIZE ;
if ( maxBytes <= 0 )
doSelectVsavData( fname, vsavData ) ;
else {
var vsavDataDecoded = atob( removeBase64Prefix( vsavData ) ) ;
if ( vsavDataDecoded.length <= 1024*maxBytes )
doSelectVsavData( fname, vsavData ) ;
else {
ask( "VASL scenario", "VASL scenario files should be less than " + maxBytes + " KB.", {
ok: function() { doSelectVsavData( fname, vsavData ) ; },
} ) ;
}
}
} ;
fileReader.readAsDataURL( file ) ;
}
function doSelectVsavData( fname, vsavData ) {
// show the file details in the UI
$gVsavContainer.find( ".hint" ).hide() ;
var $fileInfo = $gVsavContainer.find( ".file-info" ) ;
$fileInfo.find( ".name" ).text( fname ) ;
$fileInfo.show() ;
// prepare the upload
// NOTE: We do this here (rather than when the user clicks the "Upload" button),
// so that we can show the generated screenshot.
vsavData = removeBase64Prefix( vsavData ) ;
prepareUploadFiles( fname, vsavData ) ;
}
function onAddScreenshotFile() {
if ( ! canAddScreenshotFile() )
return ;
$( "#select-screenshot-for-upload" ).trigger( "click" ) ; // nb: onSelectScreenshotFile() will be called
}
function canAddScreenshotFile() {
return $gScreenshotContainer.find( ".hint" ).css( "display" ) !== "none" ;
}
function onSelectScreenshotFile( file ) {
// read the selected image file
var fileReader = new FileReader() ;
fileReader.onload = function() {
var imageData = fileReader.result ;
var fname = file.name ;
// check the file size
var maxBytes = gAppConfig.ASA_MAX_SCREENSHOT_SIZE ;
if ( maxBytes <= 0 )
doSelectScreenshotFile() ;
else {
var imageDataDecoded = atob( removeBase64Prefix( imageData ) ) ;
if ( imageDataDecoded.length <= 1024*maxBytes )
doSelectScreenshotFile() ;
else {
ask( "VASL screenshot", "Screenshots should be less than " + maxBytes + " KB.", {
ok: doSelectScreenshotFile
} ) ;
}
}
function doSelectScreenshotFile() {
// show the image preview
setScreenshotPreview( imageData, true ) ;
imageData = removeBase64Prefix( imageData ) ;
gScreenshotData = [ fname, atob(imageData) ] ;
}
} ;
fileReader.readAsDataURL( file ) ;
}
function initExternalDragDrop() {
// disable events we're not interested in
[ $gVsavContainer, $gScreenshotContainer ].forEach( function( $elem ) {
$elem.on( "dragenter", stopEvent ) ;
$elem.on( "dragleave", stopEvent ) ;
$elem.on( "dragover", stopEvent ) ;
} ) ;
// add handlers for files dropped in
$gVsavContainer.on( "drop", function( evt ) {
if ( ! canAddVsavFile() )
return ;
onSelectVsavFile( evt.originalEvent.dataTransfer.files[0] ) ;
stopEvent( evt ) ;
} ) ;
$gScreenshotContainer.on( "drop", function( evt ) {
if ( ! canAddScreenshotFile() )
return ;
onSelectScreenshotFile( evt.originalEvent.dataTransfer.files[0] ) ;
stopEvent( evt ) ;
} ) ;
}
function updateUi()
{
// update the UI
var userName = $gDialog.find( ".auth .user" ).val().trim() ;
var apiToken = $gDialog.find( ".auth .token" ).val().trim() ;
$( ".ui-dialog.scenario-upload button.upload" ).button(
userName !== "" && apiToken !== "" ? "enable" : "disable"
) ;
}
// shpw the upload dialog
$( "#scenario-upload-dialog" ).dialog( {
title: "Upload to the ASL Scenario Archive",
dialogClass: "scenario-upload",
modal: true,
width: 800, minWidth: 800,
height: 500, minHeight: 500,
create: function() {
// add handlers to add files to be uploaded
$gVsavContainer = $(this).find( ".vsav-container" ) ;
$gVsavContainer.on( "click", onAddVsavFile ) ;
$( "#select-vsav-for-upload" ).on( "change", function() {
onSelectVsavFile( $( "#select-vsav-for-upload" ).prop("files")[0] ) ;
} ) ;
$gScreenshotContainer = $(this).find( ".screenshot-container" ) ;
$gScreenshotContainer.on( "click", onAddScreenshotFile ) ;
$( "#select-screenshot-for-upload" ).on( "change", function() {
onSelectScreenshotFile( $( "#select-screenshot-for-upload" ).prop("files")[0] ) ;
} ) ;
// add handlers to remove files to be uploaded
$gVsavContainer.find( ".remove" ).on( "click", function( evt ) {
gVsavData = null ;
$gVsavContainer.find( ".file-info" ).hide() ;
$gVsavContainer.find( ".remove" ).hide() ;
$gVsavContainer.find( ".hint" ).show() ;
setScreenshotPreview( null ) ;
stopEvent( evt ) ;
} ) ;
$gScreenshotContainer.find( ".remove" ).on( "click", function( evt ) {
setScreenshotPreview( null ) ;
stopEvent( evt ) ;
} ) ;
// add keyboard handlers
$(this).find( ".auth .user" ).on( "keyup", updateUi ) ;
$(this).find( ".auth .token" ).on( "keyup", updateUi ) ;
// initialize
initExternalDragDrop() ;
},
open: function() {
// initialize
$gDialog = $(this) ;
$gDialog.find( ".auth .user" ).val( gUserSettings["asa-user-name"] ) ;
$gDialog.find( ".auth .token" ).val( gUserSettings["asa-api-token"] ? atob(gUserSettings["asa-api-token"]) : "" ) ;
$gVsavContainer.find( ".hint" ).show() ;
$gVsavContainer.find( ".file-info" ).hide() ;
$gScreenshotContainer.find( ".hint" ).show() ;
$gScreenshotContainer.find( ".preview" ).hide() ;
// initialize
gVsavData = gScreenshotData = null ;
// load the dialog
var scenarioName = $("input[name='SCENARIO_NAME']").val().trim() ;
if ( scenarioName )
$gDialog.find( ".scenario-name" ).text( scenarioName ) ;
var scenarioId = $("input[name='SCENARIO_ID']").val().trim() ;
if ( scenarioId )
$gDialog.find( ".scenario-id" ).text( "(" + scenarioId + ")" ) ;
$gDialog.find( ".asa-id" ).text( "(#" + asaScenarioId + ")" ) ;
var url = gAppConfig.ASA_SCENARIO_URL.replace( "{ID}", asaScenarioId ) ;
$gDialog.find( ".disclaimer a.asa-scenario" ).attr( "href", url ) ;
// update the UI
fixup_external_links( $gDialog ) ;
addAsaCreditPanel( $(".ui-dialog.scenario-upload"), asaScenarioId ) ;
var $btnPane = $( ".ui-dialog.scenario-upload .ui-dialog-buttonpane" ) ;
var $btn = $btnPane.find( "button.upload" ) ;
$btn.prepend(
$( "<img src='" + gImagesBaseUrl+"/upload.png" + "' style='height:0.8em;margin:0 0.35em -1px 0;'>" )
) ;
onResize() ;
updateUi() ;
},
resize: onResize,
buttons: {
Upload: { text: "Upload", class: "upload", click: function() {
uploadFiles( asaScenarioId ) ;
} },
Cancel: function() {
$gDialog.dialog( "close" ) ;
},
},
} ) ;
} ;
// --------------------------------------------------------------------
function uploadFiles( asaScenarioId )
{
// check if a full set of files is being uploaded
var warningMsg, width ;
if ( ! gVsavData && ! gScreenshotData ) {
warningMsg = "<p> Only the <em>" + gAppConfig.APP_NAME + "</em> setup will be uploaded." +
"<p> Do you want to skip uploading a VASL setup and screenshot?" ;
width = 480 ;
} else if ( ! gVsavData )
warningMsg = "Do you want to skip uploading a VASL setup?" ;
else if ( ! gScreenshotData )
warningMsg = "Do you want to skip uploading a screenshot of the VASL setup?" ;
if ( ! warningMsg ) {
// yup - just do it
doUploadFiles() ;
} else {
// nope - confirm with the user first
ask( "Incomplete upload", warningMsg, {
width: width,
ok: doUploadFiles,
} ) ;
}
function doUploadFiles() {
// unload the vasl-templates setup
var vtSetup = unload_params_for_save( true ) ;
delete vtSetup.VICTORY_CONDITIONS ;
delete vtSetup.SSR ;
// unload the authentication details
var userName = $gDialog.find( ".auth .user" ).val().trim() ;
var apiToken = $gDialog.find( ".auth .token" ).val().trim() ;
gUserSettings["asa-user-name"] = userName ;
gUserSettings["asa-api-token"] = btoa( apiToken ) ;
save_user_settings() ;
// generate a unique prefix for the filenames
// NOTE: We want to upload the files as a group, so that when we get them back, we can tell
// which screenshots go with which VASL/vasl-templates setups. The ASL Scenario Archive doesn't
// provide a mechanism for doing this, so we do it by adding a prefix to the filenames.
// This is separated from the real filename with a pipe character, so that we (and the website)
// can figure out what the real filename is.
// NOTE: This is not actually necssary any more, since the ASL Scenario Archive only maintains
// the most recently uploaded group of files for a given user+scenario, but it's not a bad idea
// for us to keep doing this.
var prefix = userName + ":" + Math.floor(Date.now()/1000) ;
// prepare the upload
var formData = new FormData() ;
formData.append( "vt_setup",
makeBlob( JSON.stringify( vtSetup, null, 4 ), "application/json" ),
prefix + "|" + "scenario.json"
) ;
if ( gVsavData ) {
formData.append( "vasl_setup",
makeBlob( gVsavData[1] ),
prefix + "|" + gVsavData[0]
) ;
}
if ( gScreenshotData ) {
formData.append( "screenshot",
makeBlob( gScreenshotData[1] ),
prefix + "|" + gScreenshotData[0]
) ;
}
// upload the files
var url = gAppConfig.ASA_UPLOAD_URL ;
if ( getUrlParam( "vsav_persistence" ) ) {
// NOTE: We are in test mode - always upload to our own test endpoint.
url = "/test-asa-upload/{ID}?user={USER}&token={TOKEN}" ;
}
url = url.replace( "{ID}", asaScenarioId ).replace( "{USER}", userName ).replace( "{TOKEN}", apiToken ) ;
var $dlg = _show_vassal_shim_progress_dlg( "Uploading your scenario..." ) ;
$.ajax( {
url: url,
method: "POST",
data: formData,
dataType: "json",
contentType: false,
processData: false,
} ).done( function( resp ) {
// check the response
$dlg.dialog( "close" ) ;
if ( resp.result.status == "ok" ) {
var msg = resp.result.message ? resp.result.message.replace("1 file(s)","1 file") : "The scenario was uploaded OK." ;
showInfoMsg( msg ) ;
// all done - we can close the dialog now
// NOTE: While the uploaded files are available on the website immediately, we normally wouldn't
// see them here for quite a while (since we need to wait until we download a new copy of the scenarios).
// This is a little unsatisfactory - the user would like to see their uploads here immediately - so we
// notify the back-end, and it will get a fresh copy of just this scenario. Other users still won't see
// the new files until they download a new copy of the scenario index, but there's not much we can do
// about that. Since this is all happening in the background, we can still close the dialog and return
// to the user, and only show a notification if something goes wrong.
onSuccessfulUpload() ;
$gDialog.dialog( "close" ) ;
} else if ( resp.result.status == "warning" )
showWarningMsg( resp.result.message ) ;
else if ( resp.result.status == "error" )
showErrorMsg( resp.result.message ) ;
else
showErrorMsg( "Unknown response status: " + resp.result.status ) ;
} ).fail( function( xhr, status, errorMsg ) {
// the upload failed - report the error
$dlg.dialog( "close" ) ;
showErrorMsg( "Can't upload the scenario:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
function onSuccessfulUpload() {
// notify the backend that the files were uploaded successfully
$.ajax( {
url: gOnSuccessfulAsaUploadUrl.replace( "ID", asaScenarioId ),
} ).done( function( resp ) {
if ( resp.status !== "ok" ) {
showWarningMsg( "Couldn't update the local scenario index:" +
"<div class='pre'>" + escapeHTML(resp.message) + "</div>"
) ;
}
} ).fail( function( xhr, status, errorMsg ) {
showErrorMsg( "Couldn't update the local scenario index:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
}
// --------------------------------------------------------------------
function prepareUploadFiles( vsavFilename, vsavData )
{
function removeLoadingSpinner() {
$gScreenshotContainer.find( ".preview" ).hide() ;
$gScreenshotContainer.find( ".hint" ).show() ;
// NOTE: This function is called if the prepare failed, so we want to show
// the "remove" button, so the user can remove the (possibly) invalid VSAV file.
$gVsavContainer.find( ".remove" ).show() ;
}
// send a request to the backend to prepare the files
setScreenshotPreview( gImagesBaseUrl + "/loader.gif", false ) ;
var data = {
filename: vsavFilename,
vsav_data: vsavData,
} ;
$.ajax( {
url: gPrepareAsaUploadUrl,
type: "POST",
data: JSON.stringify( data ),
contentType: "application/json",
} ).done( function( resp ) {
// check the response
data = _check_vassal_shim_response( resp, "Can't prepare the VASL scenario." ) ;
if ( ! data ) {
removeLoadingSpinner() ;
return ;
}
// save the prepared files
gVsavData = [ resp.filename, atob(resp.stripped_vsav) ] ;
$gVsavContainer.find( ".remove" ).show() ;
if ( resp.screenshot ) {
gScreenshotData = [ "auto-generated.jpg", atob(resp.screenshot) ] ;
setScreenshotPreview( "data:image/png;base64,"+resp.screenshot, true ) ;
} else {
showMsgDialog( "Screenshot error",
"<p> <img src='" + gImagesBaseUrl+"/vassal-screenshot-hint.png" + "' style='height:12em;float:left;margin-right:1em;'>" +
"Couldn't automatically generate a screenshot for the scenario." +
"<p> Load the scenario into VASSAL and create one manually, then add it here.",
550
) ;
removeLoadingSpinner() ;
}
} ).fail( function( xhr, status, errorMsg ) {
// the prepare failed - report the error
removeLoadingSpinner() ;
showErrorMsg( "Can't prepare the VASL scenario:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
// --------------------------------------------------------------------
function setScreenshotPreview( imageData, isPreviewImage )
{
// check if we should clear the current image preview
if ( ! imageData ) {
// yup - make it so
gScreenshotData = null ;
$gScreenshotContainer.find( ".preview" ).hide() ;
$gScreenshotContainer.find( ".remove" ).hide() ;
$gScreenshotContainer.find( ".hint" ).show() ;
return ;
}
// load the screenshot preview image
$gScreenshotContainer.find( ".hint" ).hide() ;
var $preview = $gScreenshotContainer.find( ".preview" ).hide() ;
var $img = $preview.find( "img" ) ;
$img.css( "border-width", isPreviewImage ? "1px" : 0 ) ;
$img.attr( "src", imageData ).on( "load", function() {
onResize() ;
$preview.show() ;
} ) ;
// update the "remove" button
var $btn = $gScreenshotContainer.find( ".remove" ) ;
if ( isPreviewImage )
$btn.show() ;
else
$btn.hide() ;
}
function onResize()
{
// FUDGE! The screenshot container and image are set to have a height of 100%,
// but at some point, a parent element needs to have an actual height set.
$gScreenshotContainer.css( "height",
$gDialog.innerHeight() - $gScreenshotContainer.position().top - 15
) ;
}
// --------------------------------------------------------------------
} )() ; // end local namespace

@ -4,7 +4,7 @@
var gIsFirstSearch ;
var $gDialog, $gScenariosSelect, $gSearchQueryInputBox, $gScenarioCard, $gFooter ;
var $gImportControl, $gImportScenarioButton, $gConfirmImportButton, $gCancelImportButton, $gImportWarnings ;
var $gImportControl, $gDownloadsButton, $gImportScenarioButton, $gConfirmImportButton, $gCancelImportButton, $gImportWarnings ;
// At time of writing, there are ~8600 scenarios, and loading them all into a select2 makes it a bit sluggish.
// The problem is when the user types the first 1 or 2 characters of the search query, which can result in
@ -64,6 +64,7 @@ window.searchForScenario = function()
$gScenarioCard.empty() ;
$gFooter.hide() ;
$gImportWarnings.empty().hide() ;
$gDownloadsButton.button( "disable" ) ;
$gImportScenarioButton.show() ;
$gConfirmImportButton.hide() ;
$gCancelImportButton.hide() ;
@ -90,6 +91,8 @@ function initDialog( $dlg, scenarios )
fixup_external_links( $dlg ) ;
$gScenarioCard = $dlg.find( ".scenario-card" ) ;
$gImportControl = $dlg.find( ".import-control" ) ;
$gDownloadsButton = $dlg.find( "button.downloads" ).button()
.on( "click", onDownloads ) ;
$gImportScenarioButton = $dlg.find( "button.import" ).button()
.on( "click", onImportScenario ) ;
$gConfirmImportButton = $dlg.find( "button.confirm-import" ).button()
@ -157,8 +160,8 @@ function initDialog( $dlg, scenarios )
// elements in the scenario card fade in/out). We seem to get this event *after* the selection
// has already changed in the search result, so we compare the currently-selected item
// with what's currently showing in the scenario card to decide if anything's changed.
var currId = $dlg.find( ".scenario-card" ).data( "scenario" ).scenario_id ;
var $elem = $( ".select2-results__option--highlighted .search-result" ) ;
var currId = $dlg.find( ".scenario-card" ).attr( "data-id" ) ;
if ( $elem.attr( "data-id" ) != currId )
onItemSelected( $elem.attr( "data-id" ) ) ;
}
@ -348,13 +351,18 @@ function onItemSelected( scenarioId )
// load the specified scenario
if ( ! scenarioId ) {
$gScenarioCard.empty() ;
$gScenarioCard.empty().data( "scenario", null ) ;
$gDownloadsButton.button( "disable" ) ;
return ;
}
// NOTE: We pass "auto-match" as the ROAR override, to tell the server to try to find
// a matching ROAR scenario.
loadScenarioCard( $gScenarioCard, scenarioId, true, null, "auto-match", false,
function( scenario ) {
if ( scenario.downloads && scenario.downloads.length > 0 )
$gDownloadsButton.button( "enable" ).data( "scenario", $gScenarioCard.data("scenario") ) ;
else
$gDownloadsButton.button( "disable" ) ;
// update the layout
updateLayout() ;
// NOTE: We set focus to the query input box so that UP/DOWN will work
@ -426,7 +434,30 @@ function loadScenarioCard( $target, scenarioId, briefMode, scenarioDateOverride,
$(this).siblings( ".full" ).fadeIn( 250 ) ;
$gSearchQueryInputBox.focus() ;
} ) ;
$card.find( "a.map-preview" ).imageZoom( $ ) ;
function showBoardPreviews( $elem, scenario ) {
// initialize the image gallery
var data = [] ;
scenario.map_images.forEach( function( mapImage ) {
var url = mapImage.screenshot ;
var buf = [] ;
if ( mapImage.user ) {
buf.push( "Contributed by <em>", mapImage.user, "</em>" ) ;
if ( mapImage.timestamp ) {
var tstamp = new Date( mapImage.timestamp ) ;
buf.push( "<div style='font-size:80%;font-style:italic;color:#ccc;'>",
tstamp.toLocaleDateString( undefined, { day: "numeric", month: "long", year: "numeric" } ),
"</div>"
) ;
}
}
data.push( {
src: url, thumb: url,
subHtml: buf.length > 0 ? buf.join("") : url.split("/").pop(),
} ) ;
} ) ;
$elem.lightGallery( { dynamic: true, dynamicEl: data, speed: 250 } ) ;
}
// get the scenario details
getScenarioData( scenarioId, roarOverride, function( scenario ) {
@ -434,9 +465,20 @@ function loadScenarioCard( $target, scenarioId, briefMode, scenarioDateOverride,
insertPlayerFlags( $card, scenario ) ;
makeBalanceGraphs( $card, scenario, showRoarButtons ) ;
loadObaInfo( $card, scenario, scenarioDateOverride ) ;
// initialize the map previews
var $btn = $card.find( ".boards .map-previews" ) ;
if ( ! scenario.map_images || scenario.map_images.length === 0 )
$btn.hide() ;
else {
if ( scenario.map_images.length > 1 )
$btn.after( " <span class='map-preview-count'>(" + scenario.map_images.length + ")</span>" ) ;
$btn.on( "click", function() {
showBoardPreviews( $(this), scenario ) ;
} ) ;
}
// all done - load the card into the UI and notify the caller
$target.data( "scenario", scenario ) ;
$target.html( $card ).fadeIn( 100 ) ;
$target.attr( "data-id", scenarioId ) ;
onDone( scenario ) ;
$target.attr( "data-seqNo", parseInt(seqNo)+1 ) ;
} ) ;
@ -601,38 +643,34 @@ function onImportScenario()
} ) ;
}
// get the scenario details
var scenarioId = $gScenarioCard.attr( "data-id" ) ;
getScenarioData( scenarioId, "auto-match", function( scenario ) {
// got them - check if it will be a clean import
getWarnings( scenario ) ;
if ( warnings.length === 0 && warnings2.length === 0 ) {
// yup - do the import
doImportScenario( scenario ) ;
} else {
// nope - show the warnings
if ( warnings.length > 0 ) {
var buf = [
"<div class='header'>",
"<img src='" + gImagesBaseUrl + "/warning.gif'>",
"<div class='caption'> Some values in your scenario will be changed: </div>",
"</div>"
] ;
warnings.unshift( $( buf.join("") ) ) ;
}
if ( warnings2.length > 0 ) {
if ( warnings.length > 0 )
warnings2[0].css( "margin-top", "0.5em" ) ;
$.merge( warnings, warnings2 ) ;
}
$gImportWarnings.empty().append( warnings ).slideToggle( 100 ) ;
$gConfirmImportButton.data( "scenario", scenario ).show() ;
$gCancelImportButton.show() ;
$gImportScenarioButton.hide() ;
// check if it will be a clean import
var scenario = $gScenarioCard.data( "scenario" ) ;
getWarnings( scenario ) ;
if ( warnings.length === 0 && warnings2.length === 0 ) {
// yup - do the import
doImportScenario( scenario ) ;
} else {
// nope - show the warnings
if ( warnings.length > 0 ) {
var buf = [
"<div class='header'>",
"<img src='" + gImagesBaseUrl + "/warning.gif'>",
"<div class='caption'> Some values in your scenario will be changed: </div>",
"</div>"
] ;
warnings.unshift( $( buf.join("") ) ) ;
}
} ) ;
if ( warnings2.length > 0 ) {
if ( warnings.length > 0 )
warnings2[0].css( "margin-top", "0.5em" ) ;
$.merge( warnings, warnings2 ) ;
}
$gImportWarnings.empty().append( warnings ).slideToggle( 100 ) ;
$gConfirmImportButton.data( "scenario", scenario ).show() ;
$gCancelImportButton.show() ;
$gDownloadsButton.hide() ;
$gImportScenarioButton.hide() ;
}
}
function doImportScenario( scenario )
@ -683,6 +721,7 @@ function onCancelImportScenario()
}
$gConfirmImportButton.hide() ;
$gCancelImportButton.hide() ;
$gDownloadsButton.show() ;
$gImportScenarioButton.show() ;
}
@ -851,6 +890,142 @@ window.getEffectivePlayerNat = function( playerName ) {
// --------------------------------------------------------------------
function onDownloads() {
// initialize
var scenario = $gScenarioCard.data( "scenario" ) ;
var eventHandlers = new jQueryHandlers() ;
var $dlg ;
function loadFileGroups( $fgroups ) {
var $items = [] ;
scenario.downloads.forEach( function( fgroup ) {
var buf = [] ;
var url = fgroup.screenshot || gImagesBaseUrl+"/missing-image.png" ;
buf.push( "<div class='screenshot'>", "<img src='"+url+"'>", "</div>" ) ;
buf.push( "<div>" ) ;
if ( fgroup.user )
buf.push( "<div class='contrib'>", "Contributed by <span class='user'>"+fgroup.user+"</span>", "</div>" ) ;
if ( fgroup.timestamp ) {
var tstamp = new Date( fgroup.timestamp ) ;
tstamp = tstamp.toLocaleDateString( undefined, { day: "numeric", month: "long", year: "numeric" } ) ;
buf.push( "<div class='timestamp'>", tstamp, "</div>" ) ;
}
if ( fgroup.vt_setup ) {
buf.push( "<button class='vt_setup' data-url='" + fgroup.vt_setup + "' title='Import the vasl-templates setup'>",
"<img src='"+gImagesBaseUrl+"/sortable-add.png'>", "Import",
"</button>"
) ;
}
if ( fgroup.vasl_setup ) {
buf.push( "<button class='vasl_setup' data-url='" + fgroup.vasl_setup + "' title='Download the VASL setup'>",
"<img src='"+gImagesBaseUrl+"/download.png'>", "Download",
"</button>"
) ;
}
buf.push( "</div>" ) ;
var $item = $( "<li class='fgroup'>" + buf.join("") + "</li>" ) ;
$item.find( "button.vt_setup" ).on( "click", function() {
onDownloadVtSetup( fgroup.vt_setup ) ;
} ) ;
$item.find( "button.vasl_setup" ).on( "click", function() {
onDownloadVaslSetup( fgroup.vasl_setup ) ;
} ) ;
fixup_external_links( $item ) ;
$items.push( $item ) ;
} ) ;
$fgroups.html( $items ) ;
}
function onDownloadVtSetup( url ) {
// download the vasl-templates setup
var $progressDlg = _show_vassal_shim_progress_dlg( "Downloading the scenario..." ) ;
$.ajax( {
url: url, type: "GET",
} ).done( function( resp ) {
$progressDlg.dialog( "close" ) ;
// the file was downloaded OK - load it into the UI
var fname = url.split( "/" ).pop() ;
if ( ! do_load_scenario( JSON.stringify(resp), fname ) )
return ;
// all done - we can now close the downloads popup *and* the parent search dialog
$dlg.dialog( "close" ) ;
$gDialog.dialog( "close" ) ;
} ).fail( function( xhr, status, errorMsg ) {
$progressDlg.dialog( "close" ) ;
showErrorMsg( "Can't download the <em>vasl-templates</em> setup:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
function onDownloadVaslSetup( url ) {
// download the VASL setup
// FUDGE! Triggering a download (that works in both a browser window and the desktop app)
// is depressingly tricky :-( We don't want to mess with window.location, since the download
// could end up replacing the webapp in the browser :-/ Wrapping the button with an <a> tag
// sorta works, but it can cause an external browser window to open, and remain open :-/
// We download the file ourself and then ask the user to save it.
var $progressDlg = _show_vassal_shim_progress_dlg( "Downloading the VASL scenario...", 330 ) ;
$.ajax( {
url: url, type: "GET",
xhrFields: { responseType: "arraybuffer" }
} ).done( function( resp ) {
$progressDlg.dialog( "close" ) ;
// the file was downloaded OK - give it to the user to save
var fname = url.split( "/" ).pop().split( "|" ).pop() ;
if ( gWebChannelHandler ) {
var vsavData = new Uint8Array( resp ) ;
gWebChannelHandler.save_downloaded_vsav( fname,
btoa( String.fromCharCode.apply( null, vsavData ) )
) ;
} else {
download( resp, fname, "application/octet-stream" ) ;
}
} ).fail( function( xhr, status, errorMsg ) {
$progressDlg.dialog( "close" ) ;
showErrorMsg( "Can't download the VASL scenario:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
// show the dialog
$( "#scenario-downloads-dialog" ).dialog( {
dialogClass: "scenario-downloads",
title: "Downloads for this scenario:",
modal: true,
width: 450, minWidth: 300,
height: 200, minHeight: 150,
draggable: false,
closeOnEscape: false,
open: function() {
$dlg = $(this) ;
$dlg.parent().draggable() ;
eventHandlers.addHandler( $(".ui-widget-overlay"), "click", function( evt ) {
$dlg.dialog( "close" ) ; // nb: clicking outside the popup closes it
} ) ;
// load the available downloads
var $fgroups = $dlg.find( ".fgroups" ) ;
loadFileGroups( $fgroups ) ;
$(this).css( "height", Math.min( $fgroups.outerHeight(), 400 ) ) ;
var $parentDlg = $( ".ui-dialog.scenario-search" ) ;
$( ".ui-dialog.scenario-downloads" ).position( {
my: "right bottom", at: "right top-2", of: $parentDlg.find( "button.downloads" )
} ) ;
// add a keyboard handler for ESCAPE
eventHandlers.addHandler( $(document), "keydown", function( evt ) {
if ( evt.keyCode == $.ui.keyCode.ESCAPE ) {
$dlg.dialog( "close" ) ;
stopEvent( evt ) ;
}
} ) ;
},
close: function() {
// clean up
eventHandlers.cleanUp() ;
},
} ) ;
}
// --------------------------------------------------------------------
function updateLayout()
{
// resize and position the search select2
@ -1043,18 +1218,7 @@ window.showScenarioInfo = function()
height: $(window).height() * 0.8,
minHeight: 300,
create: function() {
// add the credit panel
var $btnPane = $( ".ui-dialog.scenario-info .ui-dialog-buttonpane" ) ;
var buf = [ "<div class='credit'>",
"<a href='#'>", "<img src='" + gImagesBaseUrl+"/asl-scenario-archive.png" + "'>", "</a>",
"<div class='caption'>",
"<a href='#'>", "Information provided by", "<br>", "the ASL Scenario Archive.", "</a>",
"</div>",
"</div>"
] ;
var $credit = $( buf.join("") ) ;
fixup_external_links( $credit ) ;
$btnPane.prepend( $credit ) ;
addAsaCreditPanel( $(".ui-dialog.scenario-info"), null ) ;
},
open: function() {
// initialize
@ -1068,14 +1232,21 @@ window.showScenarioInfo = function()
var url = gAppConfig.ASA_SCENARIO_URL.replace( "{ID}", scenarioId ) ;
var $btnPane = $( ".ui-dialog.scenario-info .ui-dialog-buttonpane" ) ;
$btnPane.find( ".credit a" ).attr( "href", url ) ;
// configure the "unlink scenario" button
var $btn = $btnPane.find( "button.unlink" ) ;
// configure the "upload scenario" button
var $btn = $btnPane.find( "button.upload" ) ;
$btn.prepend(
$( "<img src='" + gImagesBaseUrl+"/cross.png" + "' style='height:0.75em;margin-right:0.35em;'>" )
$( "<img src='" + gImagesBaseUrl+"/upload.png" + "' style='height:0.9em;margin:0 0.35em -1px 0;'>" )
) ;
var creditWidth = $btnPane.find( ".credit" ).outerWidth() ;
$btn.css( { float: "left", position: "absolute", left: creditWidth+20, padding: "2px 5px" } ) ;
$btn.attr( "title", "Unlink your scenario from the ASL Scenario Archive" ) ;
$btn.css( { position: "absolute", left: creditWidth+20, padding: "2px 5px" } ) ;
$btn.attr( "title", "Upload your setup to the ASL Scenario Archive" ) ;
// configure the "unlink scenario" button
var $btn2 = $btnPane.find( "button.unlink" ) ;
$btn2.prepend(
$( "<img src='" + gImagesBaseUrl+"/cross.png" + "' style='height:0.6em;margin-right:0.35em;padding-bottom:1px;'>" )
) ;
$btn2.css( { position: "absolute", left: creditWidth+40+$btn.outerWidth(), padding: "2px 5px" } ) ;
$btn2.attr( "title", "Unlink your scenario from the ASL Scenario Archive" ) ;
// update the layout
onResize() ;
eventHandlers.addHandler( $(document), "keydown", function( evt ) {
@ -1108,6 +1279,10 @@ window.showScenarioInfo = function()
updateForConnectedScenario( null, null ) ;
$dlg.dialog( "close" ) ;
} },
Upload: { text: "Upload", class: "upload", click: function() {
$dlg.dialog( "close" ) ;
uploadScenario() ;
} },
},
} ) ;
},
@ -1171,11 +1346,11 @@ function getScenarioIndex( onReady )
// nope - download it (nb: we do this on-demand, instead of during startup,
// to give the backend time if it wants to download a fresh copy).
$.getJSON( gGetScenarioIndexUrl, function( resp ) {
if ( resp.error ) {
var msg = resp.error ;
if ( resp.warning ) {
var msg = resp.warning ;
if ( resp.message )
msg += "<div class='pre'>" + escapeHTML(resp.message) + "</div>" ;
showErrorMsg( msg ) ;
showWarningMsg( msg ) ;
return ;
}
_scenarioIndex = resp ;
@ -1206,4 +1381,31 @@ function getScenarioData( scenarioId, roarOverride, onReady )
// --------------------------------------------------------------------
window.addAsaCreditPanel = function( $dlg, scenarioId )
{
// create the credit panel
var url = scenarioId ? gAppConfig.ASA_SCENARIO_URL.replace( "{ID}", scenarioId ) : "https://aslscenarioarchive.com" ;
var buf = [ "<div class='credit'>",
"<a href='"+url+"'>", "<img src='" + gImagesBaseUrl+"/asl-scenario-archive.png" + "'>", "</a>",
"<div class='caption'>",
"<a href='"+url+"'>", "Information provided by", "<br>", "the ASL Scenario Archive.", "</a>",
"</div>",
"</div>"
] ;
var $credit = $( buf.join("") ) ;
$credit.css( { float: "left", "margin-right": "0.5em", display: "flex", "align-items": "center" } ) ;
$credit.find( "img" ).css( { height: "1.4em", "margin-right": "0.5em", opacity: 0.7 } ) ;
$credit.find( ".caption" ).css( { "font-size": "70%", "line-height": "1em", "margin-top": "-4px" } ) ;
$credit.find( "a" ).css( { "text-decoration": "none", "font-style": "italic", color: "#666" } ) ;
$credit.find( "a" ).on( "click", function() { $(this).blur() ; } ) ;
// add the credit panel to the dialog's button pane
fixup_external_links( $credit ) ;
var $btnPane = $dlg.find( ".ui-dialog-buttonpane" ) ;
$btnPane.find( ".credit" ).remove() ;
$btnPane.prepend( $credit ) ;
} ;
// --------------------------------------------------------------------
} )() ; // end local namespace

@ -1469,6 +1469,7 @@ function on_load_scenario()
else {
// yup - confirm the operation
ask( "Load scenario", "<p>This scenario has been changed.<p>Do you want load another scenario, and lose your changes?", {
width: 470,
ok: do_on_load_scenario,
cancel: function() {},
} ) ;
@ -1521,11 +1522,13 @@ function do_load_scenario( data, fname )
data = JSON.parse( data ) ;
} catch( ex ) {
showErrorMsg( "Can't load the scenario file:<div class='pre'>" + escapeHTML(ex) + "</div>" ) ;
return ;
return false ;
}
do_load_scenario_data( data ) ;
gLastSavedScenarioFilename = fname ;
showInfoMsg( "The scenario was loaded." ) ;
return true ;
}
function do_load_scenario_data( params )
@ -1807,7 +1810,7 @@ function on_save_scenario()
// if the user loads a scenario from a file that is named using a non-standard convention.
}
function unload_params_for_save( user_requested )
function unload_params_for_save( includeMetadata )
{
function extract_vo_entries( key ) {
if ( !( key in params ) )
@ -1857,7 +1860,7 @@ function unload_params_for_save( user_requested )
params.SCENARIO_DATE = scenario_date.toISOString().substring( 0, 10 ) ;
// save some admin metadata
if ( user_requested ) {
if ( includeMetadata ) {
params._app_version = gAppVersion ;
var now = (new Date()).toISOString() ;
params._last_update_time = now ;

@ -173,6 +173,7 @@ $.fn.sortable2 = function( action, args )
"</div>"
] ;
ask( "Delete "+display_name[0], buf.join(""), {
width: 350,
ok: do_delete_entry,
close: function() { set_entry_colors( $entry, false ) ; },
} ) ;

@ -242,10 +242,15 @@ function close_dialog_if_no_others( $dlg )
{
// NOTE: Escape doesn't always close a dialog, so we do it ourself, but we have to handle the case
// where more than one dialog is on-screen, and only close if there isn't another dialog on top of us.
if ( $(".ui.dialog").length >= 2 )
var nDialogs = 0 ;
$( ".ui-dialog" ).each( function() {
if ( $(this).css( "display" ) !== "none" )
++ nDialogs ;
} ) ;
if ( nDialogs >= 2 )
return ;
// NOTE: We also want to stop Escape from closing a dialog if an image preview is being shown.
if ( $( ".jquery-image-zoom" ).length > 0 )
if ( $( ".lg-outer" ).length > 0 )
return ;
// close the dialog
@ -264,6 +269,7 @@ function ask( title, msg, args )
modal: true,
closeOnEscape:false,
title: title,
width: args.width || 400,
minWidth: 250,
maxHeight: window.innerHeight,
create: function() {
@ -298,6 +304,25 @@ function ask( title, msg, args )
return false ;
}
function showMsgDialog( title, msg, width )
{
// show the message in a dialog
$( "#ask" ).dialog( {
dialogClass: "ask",
modal: true,
title: title,
width: width, minWidth: 250,
open: function() {
$(this).html( msg ) ;
},
buttons: {
OK: function() {
$(this).dialog( "close" ) ;
},
},
} ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function showInfoMsg( msg ) { doShowNotificationMsg( "info", msg ) ; }
@ -461,10 +486,15 @@ function addUrlParam( url, param, val )
}
function escapeHTML( val ) { return new Option(val).innerHTML ; }
function pluralString( n, str1, str2 ) { return (n == 1) ? str1 : str2 ; }
function trimString( val ) { return val ? val.trim() : val ; }
function fpFmt( val, nDigits ) { return val.toFixed( nDigits ) ; }
function pluralString( n, str1, str2, combine )
{
var val = (n == 1) ? str1 : str2 ;
return combine ? n + " " + val : val ;
}
function percentString( val )
{
val = Math.round( val ) ;
@ -536,6 +566,13 @@ function getFilenameExtn( fname )
return null ;
}
function removeBase64Prefix( val )
{
// remove the base64 prefix from the start of the string
// - data: MIME-TYPE ; base64 , ...
return val.replace( /^data:.*?;base64,/, "" ) ;
}
function stopEvent( evt )
{
// stop further processing for the event
@ -543,6 +580,17 @@ function stopEvent( evt )
evt.stopPropagation() ;
}
function makeBlob( data, mimeType )
{
// create a Blob from a binary string
var bytes = new Uint8Array( data.length ) ;
for ( var i=0 ; i < data.length ; ++i )
bytes[i] = data.charCodeAt( i ) ;
return new Blob( [bytes], {
type: mimeType || "application/octet-stream"
} ) ;
}
function isIE()
{
// check if we're running in IE :-/

@ -301,6 +301,7 @@ function on_analyze_vsav()
// yup - confirm the operation
ask( "Analyze VASL scenario",
"<p>There are some vehicles/ordnance already configured. <p>They will be <i>replaced</i> with those found in the analyzed VASL scenario.", {
width: 520,
ok: function() { _load_and_process_vsav( _do_analyze_vsav ) ; },
} ) ;
return ;
@ -468,9 +469,7 @@ function on_load_vsav_file_selected()
var fileReader = new FileReader() ;
var file = $("#load-vsav").prop( "files" )[0] ;
fileReader.onload = function() {
vsav_data = fileReader.result ;
if ( vsav_data.substring(0,5) === "data:" )
vsav_data = vsav_data.split( "," )[1] ;
vsav_data = removeBase64Prefix( fileReader.result ) ;
gLoadVsavHandler( vsav_data, file.name ) ;
gLoadVsavHandler = null ;
} ;
@ -529,13 +528,13 @@ function show_vassal_shim_error_dlg( resp, caption )
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function _show_vassal_shim_progress_dlg( caption )
function _show_vassal_shim_progress_dlg( caption, width )
{
// show the progress dialog
return $( "#vassal-shim-progress" ).dialog( {
dialogClass: "vassal-shim-progress",
modal: true,
width: 300,
width: width || 300,
height: 60,
resizable: false,
closeOnEscape: false,

@ -136,6 +136,7 @@ function add_vo( vo_type, player_no )
if ( usedVoIds.indexOf( sel_entry.id ) !== -1 ) {
var vo_type2 = SORTABLE_DISPLAY_NAMES[ "ob_" + vo_type ][0] ;
ask( "Add "+vo_type2, "<p>This "+vo_type2+" is already in the OB<p>Do you want to add it again?", {
width: 300,
ok: add_sel_entry,
} ) ;
return ;

@ -10,10 +10,10 @@
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='jquery-ui/jquery-ui.min.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='growl/jquery.growl.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='popmenu/jquery.popmenu.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='imageZoom/jquery.imageZoom.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='spectrum/spectrum.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='select2/select2.min.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='chartjs/Chart.min.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='lightgallery/css/lightgallery.min.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/main.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/tabs.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/tabs-scenario.css')}}" />
@ -32,6 +32,8 @@
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/snippets.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/scenario-search-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/scenario-card.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/scenario-upload-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/scenario-downloads-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/user-settings-dialog.css')}}" />
</head>
@ -58,6 +60,8 @@
<input id="load-template-pack" type="file" accept=".zip,.j2" style="display:none;">
<input id="load-vsav" type="file" accept=".vsav" style="display:none;">
<input id="load-vlog" type="file" multiple accept=".vlog" style="display:none;">
<input id="select-vsav-for-upload" type="file" multiple accept=".vsav" style="display:none;">
<input id="select-screenshot-for-upload" type="file" multiple accept=".png,.jpg,.jpeg,.gif" style="display:none;">
<div id="alt-webapp-base-url" style="display:none;"> Webapp base URL: </div>
@ -77,6 +81,8 @@
{%include "edit-vo-dialog.html"%}
{%include "scenario-info-dialog.html"%}
{%include "scenario-search-dialog.html"%}
{%include "scenario-upload-dialog.html"%}
{%include "scenario-downloads-dialog.html"%}
{%include "select-roar-scenario-dialog.html"%}
{%include "vassal.html"%}
@ -97,7 +103,6 @@
<script src="{{url_for('static',filename='jinja/jinja.js')}}"></script>
<script src="{{url_for('static',filename='growl/jquery.growl.js')}}"></script>
<script src="{{url_for('static',filename='popmenu/jquery.popmenu-1.0.0.min.js')}}"></script>
<script src="{{url_for('static',filename='imageZoom/jquery.imageZoom.min.js')}}"></script>
<script src="{{url_for('static',filename='hotkey/jquery.hotkey.js')}}"></script>
<script src="{{url_for('static',filename='spectrum/spectrum.js')}}"></script>
<script src="{{url_for('static',filename='download/download.min.js')}}"></script>
@ -107,6 +112,10 @@
<script src="{{url_for('static',filename='js-cookie/js.cookie.js')}}"></script>
<script src="{{url_for('static',filename='chartjs/Chart.min.js')}}"></script>
<script src="{{url_for('static',filename='chartjs/chartjs-plugin-labels.min.js')}}"></script>
<script src="{{url_for('static',filename='lightgallery/js/lightgallery.min.js')}}"></script>
<script src="{{url_for('static',filename='lightgallery/js/lg-thumbnail.min.js')}}"></script>
<script src="{{url_for('static',filename='lightgallery/js/lg-zoom.min.js')}}"></script>
<script src="{{url_for('static',filename='lightgallery/js/lg-rotate.min.js')}}"></script>
<script>
gAppName = "{{APP_NAME}}" ;
@ -128,6 +137,8 @@ gGetRoarScenarioIndexUrl = "{{url_for('get_roar_scenario_index')}}" ;
gAnalyzeVsavUrl = "{{url_for('analyze_vsav')}}" ;
gAnalyzeVlogsUrl = "{{url_for('analyze_vlogs')}}" ;
gUpdateVsavUrl = "{{url_for('update_vsav')}}" ;
gPrepareAsaUploadUrl = "{{url_for('prepare_asa_upload')}}" ;
gOnSuccessfulAsaUploadUrl = "{{url_for('on_successful_asa_upload',scenario_id='ID')}}" ;
gMakeSnippetImageUrl = "{{url_for('make_snippet_image')}}" ;
gHelpUrl = "{{url_for('show_help')}}" ;
</script>
@ -135,6 +146,7 @@ gHelpUrl = "{{url_for('show_help')}}" ;
<script src="{{url_for('static',filename='main.js')}}"></script>
<script src="{{url_for('static',filename='snippets.js')}}"></script>
<script src="{{url_for('static',filename='scenarios.js')}}"></script>
<script src="{{url_for('static',filename='scenario-upload.js')}}"></script>
<script src="{{url_for('static',filename='nat_caps.js')}}"></script>
<script src="{{url_for('static',filename='extras.js')}}"></script>
<script src="{{url_for('static',filename='simple_notes.js')}}"></script>

@ -123,12 +123,11 @@
</div> <!-- end player-info -->
{%if BOARDS%} <div class="boards">
{# NOTE: We always show the boards div, even if there aren't any, since there might be preview images. #}
<div class="boards">
<b>Boards:</b> {{BOARDS}}
{%if MAP_IMAGES%} {%for map in MAP_IMAGES%}
<a href="{{map}}" class="map-preview"><img src="{{url_for('static',filename='images/map-preview.png')}}" title="Board preview"></a>
{%endfor%} {%endif%}
</div> {%endif%}
<img src="{{url_for('static',filename='images/map-preview.png')}}" class="map-previews" title="Board previews">
</div>
{%if OVERLAYS%} <div class="overlays"> <b>Overlays:</b> {{OVERLAYS}} </div> {%endif%}

@ -0,0 +1,5 @@
<div id="scenario-downloads-dialog" style="display:none;">
<ul class="fgroups"> </ul>
</div>

@ -21,9 +21,13 @@
<div class="warnings" style="display:none;"></div>
<div class="buttons">
<button class="import" title="Import this scenario and link it to the ASL Scenario Archive.">
<img class="logo" src="{{url_for('static',filename='images/sortable-add.png')}}">
<img src="{{url_for('static',filename='images/sortable-add.png')}}">
Import
</button>
<button class="downloads" title="Downloads for this scenario.">
<img src="{{url_for('static',filename='images/download.png')}}">
Downloads
</button>
<button class="cancel-import"> Cancel </button>
<button class="confirm-import ok"> OK </button>
</div>

@ -0,0 +1,66 @@
<div id="scenario-upload-dialog" style="display:none;">
<div class="row">
<label for="scenario"> Scenario: </label> <span class="scenario-name"></span>
<span class="scenario-id"></span>
<span class="asa-id"></span>
</div>
<div class="auth">
<div class="row">
<label for="user"> User name: </label> <input class="user" type="text" size="20">
<span class="hint"> (register for an account <a href="https://www.aslscenarioarchive.com/register.php">here</a>) <span>
</div>
<div class="row">
<label for="token"> API token: </label> <input class="token" type="text" size="20">
<span class="hint"> (get this from your account's <em>My Page</em>) </span>
</div>
</div>
<div class="grid">
<div class="left">
<div class="vsav-container">
<div class="hint">
<img src="{{url_for('static',filename='images/lfa/file.png')}}">
Click here to select your VASL save file, or drag it in.
</div>
<div class="file-info" style="display:none;">
<img src="{{url_for('static',filename='images/lfa/file.png')}}">
<div>
<span class="name"></span>
</div>
</div>
<img src="{{url_for('static',filename='images/cross.png')}}" class="remove" title="Remove this file" style="display:none;">
</div>
<div class="disclaimer">
Your <em>vasl-templates</em> setup will also be uploaded.
<div style="margin-top:0.5em;display:flex;">
<img src="{{url_for('static',filename='images/warning.gif')}}" style="height:1.5em;margin:0.1em 0.5em 0 0;">
<div> For copyright reasons, some information will be removed from the uploads (e.g. Victory Conditions, SSR's). If you are sure that the scenario is <em>not</em> copyrighted, you can upload the complete files at the <a href="" class="asa-scenario">ASL Scenario Archive</a>. </div>
</div>
</div>
</div> <!-- end left -->
<div class="right">
<div class="screenshot-container">
<div class="hint">
<img src="{{url_for('static',filename='images/screenshot.png')}}">
Click here to select a screenshot file, <br> or drag one in.
</div>
<div class="preview" style="display:none;">
<img src="">
</div>
<img src="{{url_for('static',filename='images/cross.png')}}" class="remove" title="Remove this file" style="display:none;">
</div>
</div> <!-- end right -->
</div> <!-- end grid -->
</div>

@ -0,0 +1,6 @@
{ "result": {
"status": "error",
"message": "Invalid user token"
} }

@ -0,0 +1,6 @@
{ "result": {
"status": "error",
"message": "Invalid user token"
} }

@ -0,0 +1,6 @@
{ "result": {
"status": "error",
"message": "Invalid user token"
} }

@ -0,0 +1,6 @@
{ "result": {
"status": "error",
"message": "No screenshot|vt_setup|vasl_setup uploaded"
} }

@ -0,0 +1,6 @@
{ "result": {
"status": "ok",
"message": "Uploaded files successfully"
} }

@ -29,9 +29,9 @@
"attacker": "Romanian",
"att_desc": "1st Romanian Army",
"maps": [ 1, "2", "RB" ],
"mapImages": [
"http://localhost:5010/static/images/asl-scenario-archive.png"
],
"mapImages": [ {
"url": "http://localhost:5010/static/images/asl-scenario-archive.png"
} ],
"overlays": [ 1, "2", "OG1" ],
"misc": "Some extra rules",
"errata": [
@ -42,7 +42,22 @@
"totalGames": "10",
"defender_wins": "3",
"attacker_wins": "7"
} ]
} ],
"templateImages": [
{ "url": "http://test.com/alice:1400000000|screenshot1.png", "user": "alice", "created": "2014-05-13 01:02:03" },
{ "url": "http://test.com/bob:1500000000|screenshot2.png", "user": "bob", "created": "2017-07-14 01:02:03" },
{ "url": "http://test.com/chuck:1600000000|screenshot3.png", "user": "chuck", "created": "2020-09-13 01:02:03" }
],
"vaslTemplates": [
{ "url": "http://test.com/alice:1400000000|vt_setup1.json", "user": "alice", "created": "2014-05-13 01:02:03" },
{ "url": "http://test.com/bob:1500000000|vt_setup2.json", "user": "bob", "created": "2017-07-14 01:02:03" },
{ "url": "http://test.com/dave:1700000000|vt_setup4.json", "user": "dave", "created": "2023-11-14 01:02:03" }
],
"vaslTemplateSetups": [
{ "url": "http://test.com/alice:1400000000|vt_setup1.vsav", "user": "alice", "created": "2014-05-13 01:02:03" },
{ "url": "http://test.com/chuck:1600000000|vt_setup3.vsav", "user": "chuck", "created": "2020-09-13 01:02:03" },
{ "url": "http://test.com/dave:1700000000|vt_setup4.vsav", "user": "dave", "created": "2023-11-14 01:02:03" }
]
},
{ "scenario_id": "2",

@ -329,3 +329,28 @@ class ControlTests:
else:
assert False
return self
def _reset_last_asa_upload( self ):
"""Reset the saved last upload to the ASL Scenario Archive."""
_logger.info( "Reseting the last ASA upload." )
webapp_scenarios._last_asa_upload = None #pylint: disable=protected-access
return self
def _get_last_asa_upload( self ): #pylint: disable=no-self-use
"""Get the last set of files uploaded to the ASL Scenario Archive."""
last_asa_upload = webapp_scenarios._last_asa_upload #pylint: disable=protected-access
if not last_asa_upload:
return {} # FUDGE! This is for the remote testing framework :-/
last_asa_upload = last_asa_upload.copy()
# FUDGE! We can't send binary data over the remote testing interface, but since the tests just check
# for the presence of a VASL save file and screenshot, we just send an indicator of that, and not
# the data itself. This will be reworked when we switch to using gRPC.
if "vasl_setup" in last_asa_upload:
assert last_asa_upload["vasl_setup"][:2] == b"PK"
last_asa_upload[ "vasl_setup" ] = "PK:{}".format( len(last_asa_upload["vasl_setup"]) )
if "screenshot" in last_asa_upload:
assert last_asa_upload["screenshot"][:2] == b"\xff\xd8" \
and last_asa_upload["screenshot"][-2:] == b"\xff\xd9" # nb: these are the magic numbers for JPEG's
last_asa_upload[ "screenshot" ] = "JPEG:{}".format( len(last_asa_upload["screenshot"]) )
_logger.debug( "Returning the last ASA upload: %s", last_asa_upload )
return last_asa_upload

@ -1,6 +1,9 @@
"""" Test scenario search. """
import os
import re
import base64
import time
import pytest
from selenium.webdriver.common.action_chains import ActionChains
@ -10,7 +13,7 @@ from selenium.common.exceptions import StaleElementReferenceException
from vasl_templates.webapp.tests.test_scenario_persistence import save_scenario, load_scenario
from vasl_templates.webapp.tests.utils import init_webapp, select_tab, new_scenario, \
set_player, set_template_params, set_scenario_date, get_player_nat, get_theater, set_theater, \
wait_for, wait_for_elem, find_child, find_children, get_css_classes
wait_for, wait_for_elem, find_child, find_children, get_css_classes, set_stored_msg
# ---------------------------------------------------------------------
@ -46,8 +49,13 @@ def test_scenario_cards( webapp, webdriver ):
"oba": [
[ "Dutch", "1B", "1R" ], [ "Romanian", "-", "-" ]
],
"boards": "1, 2, RB",
"map_previews": [ "asl-scenario-archive.png" ],
"boards": "1, 2, RB (4)", # n: the (4) is the number of map previews
"map_previews": [
"chuck:1600000000|screenshot3.png",
"bob:1500000000|screenshot2.png",
"alice:1400000000|screenshot1.png",
"asl-scenario-archive.png"
],
"overlays": "1, 2, OG1",
"extra_rules": "Some extra rules",
"errata": [
@ -196,7 +204,8 @@ def test_import_warnings( webapp, webdriver ): #pylint: disable=too-many-stateme
warnings = wait_for( 2, lambda: find_children( ".warnings input[type='checkbox']", dlg ) )
assert [ w.get_attribute( "name" ) for w in warnings ] == [ "defender_name" ]
assert not warnings[0].is_selected()
wait_for( 2, lambda: warnings[0].is_displayed() )
wait_for( 2, lambda: warnings[0].is_displayed )
time.sleep( 0.1 ) # nb: wait for the slide to finish :-/
warnings[0].click()
find_child( "button.confirm-import", dlg ).click()
assert not dlg.is_displayed()
@ -532,6 +541,162 @@ def test_scenario_linking( webapp, webdriver ):
# ---------------------------------------------------------------------
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member
def test_scenario_upload( webapp, webdriver ):
"""Test uploading scenarios to the ASL Scenario Archive."""
# initialize
control_tests = init_webapp( webapp, webdriver, vsav_persistence=1, scenario_persistence=1 )
def do_upload( prep_upload, expect_ask ):
"""Upload the scenario to our test endpoint."""
# show the scenario card
find_child( "button.scenario-search" ).click()
wait_for( 2, _find_scenario_card )
# open the upload dialog
find_child( ".ui-dialog.scenario-info button.upload" ).click()
dlg = wait_for_elem( 2, ".ui-dialog.scenario-upload" )
if prep_upload:
prep_upload( dlg )
# start the upload
control_tests.reset_last_asa_upload()
find_child( "button.upload", dlg ).click()
if expect_ask:
dlg = wait_for_elem( 2, ".ui-dialog.ask" )
find_child( "button.ok", dlg ).click()
# wait for the upload to be processed
asa_upload = wait_for( 5, control_tests.get_last_asa_upload )
assert asa_upload["user"] == user_name
assert asa_upload["token"] == api_token
return asa_upload
user_name, api_token = "joe", "xyz123"
def prep_upload( dlg ):
"""Prepare the upload."""
assert find_child( ".scenario-name", dlg ).text == "Full content scenario"
assert find_child( ".scenario-id", dlg ).text == "(FCS-1)"
assert find_child( ".asa-id", dlg ).text == "(#1)"
# NOTE: We only set the auth details once, then they should be remembered.
find_child( "input.user", dlg ).send_keys( user_name )
find_child( "input.token", dlg ).send_keys( api_token )
# test uploading just a vasl-templates setup
dlg = _do_scenario_search( "full", [1], webdriver )
find_child( "button.import", dlg ).click()
asa_upload = do_upload( prep_upload, True )
assert asa_upload["user"] == user_name
assert asa_upload["token"] == api_token
assert "vasl_setup" not in asa_upload
assert "screenshot" not in asa_upload
# compare the vasl-templates setup
saved_scenario = save_scenario()
keys = [ k for k in saved_scenario if k.startswith("_") ]
for key in keys:
del saved_scenario[ key ]
del asa_upload["vt_setup"][ key ]
del saved_scenario[ "VICTORY_CONDITIONS" ]
del saved_scenario[ "SSR" ]
assert asa_upload["vt_setup"] == saved_scenario
def prep_upload2( dlg ):
"""Prepare the upload."""
assert asa_upload["user"] == user_name
assert asa_upload["token"] == api_token
# send the VSAV data to the front-end
fname = os.path.join( os.path.dirname(__file__), "fixtures/update-vsav/full.vsav" )
vsav_data = open( fname, "rb" ).read()
set_stored_msg( "_vsav-persistence_", base64.b64encode( vsav_data ).decode( "utf-8" ) )
find_child( ".vsav-container", dlg ).click()
# wait for the files to be prepared
wait_for( 30,
lambda: "loader.gif" not in find_child( ".screenshot-container .preview img" ).get_attribute( "src" )
)
# test uploading a VASL save file
def do_test(): #pylint: disable=missing-docstring
dlg = _do_scenario_search( "full", [1], webdriver )
find_child( "button.import", dlg ).click()
asa_upload = do_upload( prep_upload2, False )
assert isinstance( asa_upload["vt_setup"], dict )
assert asa_upload["vasl_setup"][:3] == "PK:"
assert asa_upload["screenshot"][:5] == "JPEG:"
from vasl_templates.webapp.tests.test_vassal import _run_tests
_run_tests( control_tests, do_test, True )
# ---------------------------------------------------------------------
def test_scenario_downloads( webapp, webdriver ):
"""Test downloading files from the ASL Scenario Archive."""
# initialize
init_webapp( webapp, webdriver )
def unload_downloads():
"""Unload the available downloads."""
btn = find_child( ".ui-dialog.scenario-search .import-control button.downloads" )
if not btn.is_enabled():
return None
btn.click()
dlg = wait_for_elem( 2, "#scenario-downloads-dialog" )
# unload the file groups
fgroups = []
for elem in find_children( ".fgroup", dlg ):
fgroup = {
"screenshot": fixup( find_child( ".screenshot img", elem ).get_attribute( "src" ) ),
"user": fixup( find_child( ".user", elem ).text ),
"timestamp": fixup( find_child( ".timestamp", elem ).text ),
}
add_download_url( fgroup, "vt_setup", elem )
add_download_url( fgroup, "vasl_setup", elem )
fgroups.append( fgroup )
return fgroups
def add_download_url( fgroup, key, parent ): #pylint: disable=missing-docstring
btn = find_child( "."+key, parent )
if btn:
fgroup[ key ] = fixup( btn.get_attribute( "data-url" ) )
def fixup( val ): #pylint: disable=missing-docstring
val = re.sub( r"(localhost|127.0.0.1):\d+", "{LOCALHOST}", val )
val = val.replace( "%7C", "|" )
return val
# check the downloads for a scenario that doesn't have any
_do_scenario_search( "fighting", [2], webdriver )
assert unload_downloads() is None
# check the downloads for a scenario that has some
_do_scenario_search( "full", [1], webdriver )
assert unload_downloads() == [ {
"screenshot": "http://{LOCALHOST}/static/images/missing-image.png",
"timestamp": "November 14, 2023",
"user": "dave",
"vasl_setup": "http://test.com/dave:1700000000|vt_setup4.vsav",
"vt_setup": "http://test.com/dave:1700000000|vt_setup4.json"
}, {
"screenshot": "http://test.com/chuck:1600000000|screenshot3.png",
"timestamp": "September 13, 2020",
"user": "chuck",
"vasl_setup": "http://test.com/chuck:1600000000|vt_setup3.vsav"
}, {
"screenshot": "http://test.com/bob:1500000000|screenshot2.png",
"timestamp": "July 14, 2017",
"user": "bob",
"vt_setup": "http://test.com/bob:1500000000|vt_setup2.json"
}, {
"screenshot": "http://test.com/alice:1400000000|screenshot1.png",
"timestamp": "May 13, 2014",
"user": "alice",
"vasl_setup": "http://test.com/alice:1400000000|vt_setup1.vsav",
"vt_setup": "http://test.com/alice:1400000000|vt_setup1.json"
} ]
# ---------------------------------------------------------------------
def _do_scenario_search( query, expected, webdriver ):
"""Do a scenario search."""
@ -601,7 +766,9 @@ def _unload_scenario_card(): #pylint: disable=too-many-branches,too-many-locals
if trim_postfix:
assert val.endswith( trim_postfix )
val = val[ : -len(trim_postfix) ]
results[ key ] = val.strip()
val = val.strip()
if val:
results[ key ] = val
def unload_attr( key, sel, attr ):
"""Unload a element's attribute from the scenario card."""
elem = find_child( sel, card )
@ -675,10 +842,19 @@ def _unload_scenario_card(): #pylint: disable=too-many-branches,too-many-locals
results[ "oba" ] = oba_info
# unload any map preview images
elems = find_children( ".map-preview", card )
if elems:
urls = [ e.get_attribute("href") for e in elems ]
results[ "map_previews" ] = [ os.path.basename(u) for u in urls ]
btn = find_child( ".map-previews", card )
if btn and btn.is_displayed():
btn.click()
imgs = find_children( ".lg .lg-thumb-item img" )
if not imgs:
# NOTE: If there is only one image, no thumbnails are shown - just use the main image
imgs = [ find_child( ".lg .lg-image" ) ]
urls = [ e.get_attribute( "src" ) for e in imgs ]
results[ "map_previews" ] = [
os.path.basename( u ).replace( "%7C", "|" )
for u in urls
]
find_child( ".lg .lg-close" ).click()
# unload any errata
def get_source( val ): #pylint: disable=missing-docstring

@ -9,7 +9,7 @@ import math
from collections import defaultdict
from flask import request, Response, send_file
from PIL import Image
from PIL import Image, ImageChops
# ---------------------------------------------------------------------
@ -159,6 +159,31 @@ def resize_image_response( resp, default_width=None, default_height=None, defaul
# nope - return the image as-is
return resp
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def trim_image( img ):
"""Trim whitespace from an image."""
if isinstance( img, str ):
img = Image.open( img )
# trim the screenshot (nb: we assume a white background)
bgd = Image.new( img.mode, img.size, (255,255,255,255) )
diff = ImageChops.difference( img, bgd )
bbox = diff.getbbox()
return img.crop( bbox )
def get_image_data( img, **kwargs ):
"""Get the data from a Pillow image."""
buf = io.BytesIO()
img.save( buf, format=kwargs.pop("format","PNG"), **kwargs )
buf.seek( 0 )
return buf.read()
def remove_alpha_from_image( img ):
"""Remove the alpha channel from an image."""
img2 = Image.new( "RGB", img.size, "WHITE" )
img2.paste( img, (0,0), img )
return img2
# ---------------------------------------------------------------------
def change_extn( fname, extn ):

@ -345,6 +345,12 @@ class VassalShim:
"analyzeLogs", *fnames
)
def prepare_asa_upload( self, vsave_fname, stripped_vsav_fname, screenshot_fname ):
"""Prepare files for upload to the ASL Scenario Archive."""
return self._run_vassal_shim(
"prepareUpload", vsave_fname, stripped_vsav_fname, screenshot_fname
)
def _run_vassal_shim( self, *args ): #pylint: disable=too-many-locals
"""Run the VASSAL shim."""
@ -367,7 +373,7 @@ class VassalShim:
java_path, "-classpath", class_path, "vassal_shim.Main",
args[0]
]
if args[0] in ("dump","analyze","analyzeLogs","update"):
if args[0] in ("dump","analyze","analyzeLogs","update","prepareUpload"):
if not globvars.vasl_mod:
raise SimpleError( "The VASL module has not been configured." )
args2.append( globvars.vasl_mod.filename )
@ -465,10 +471,10 @@ class VassalShim:
"<p>If this problem persists, try configuring a longer timeout."
} )
if isinstance( ex, SimpleError ):
logger.error( "VSAV update error: %s", ex )
logger.error( "VASSAL shim error: %s", ex )
return jsonify( { "error": str(ex) } )
logger.error( "Unexpected VSAV update error: %s", ex )
logger.error( "Unexpected VASSAL shim error: %s", ex )
return jsonify( {
"error": str(ex),
"stdout": traceback.format_exc(),

@ -6,11 +6,10 @@ import tempfile
import atexit
import logging
from PIL import Image, ImageChops
from selenium import webdriver
from vasl_templates.webapp import app, globvars
from vasl_templates.webapp.utils import TempFile, SimpleError
from vasl_templates.webapp.utils import TempFile, SimpleError, trim_image
_logger = logging.getLogger( "webdriver" )
@ -110,12 +109,7 @@ class WebDriver:
def do_get_screenshot( fname ): #pylint: disable=missing-docstring
self.driver.save_screenshot( fname )
img = Image.open( fname )
# trim the screenshot (nb: we assume a white background)
bgd = Image.new( img.mode, img.size, (255,255,255,255) )
diff = ImageChops.difference( img, bgd )
bbox = diff.getbbox()
return img.crop( bbox )
return trim_image( fname )
with TempFile( extn=".html", mode="w", encoding="utf-8" ) as html_tempfile, \
TempFile( extn=".png" ) as screenshot_tempfile:

@ -0,0 +1,56 @@
package vassal_shim ;
import java.io.File ;
import VASSAL.build.module.map.ImageSaver ;
import VASSAL.build.module.Map ;
import VASSAL.tools.swing.ProgressDialog ;
// --------------------------------------------------------------------
public class AppImageSaver extends ImageSaver
{
// FUDGE! We implement our own version of ImageSaver so that we can get access
// to its protected member variables :-/
// FUDGE! VASSAL's ImageSaver shows a progress dialog as the screenshot is generated,
// so we need to provide one of these to stop it from crashing :-/
// We detect when the process has finished when VASSAL "closes" the dialog.
class DummyProgressDialog extends ProgressDialog
{
private boolean isDone ;
public DummyProgressDialog() {
super( null, "", "" ) ;
isDone = false ;
}
public void dispose() {
isDone = true ;
super.dispose() ;
}
public boolean isDone() { return isDone ; }
}
public AppImageSaver( Map map )
{
// initialize
super( map ) ;
}
public void generateScreenshot( File outputFile, int width, int height, int timeout )
throws InterruptedException
{
// install our dummy progress dialog into ImageSaver
dialog = new DummyProgressDialog() ;
// call into VASSAL to generate the screenshot
super.writeMapRectAsImage( outputFile, 0, 0, width, height ) ;
// wait for the task to finish
for ( int i=0 ; i < timeout ; ++i ) {
if ( ((DummyProgressDialog)dialog).isDone() )
return ;
Thread.sleep( 1*1000 ) ;
}
throw new RuntimeException( "Screenshot timeout." ) ;
}
}

@ -50,6 +50,18 @@ public class Main
shim.updateScenario( args[3], args[4], args[5], args[6] ) ;
System.exit( 0 ) ;
}
else if ( cmd.equals( "screenshot" ) ) {
checkArgs( args, 4, false, "the VASL .vmod file, scenario file and output file" ) ;
VassalShim shim = new VassalShim( args[1], null ) ;
shim.takeScreenshot( args[2], args[3] ) ;
System.exit( 0 ) ;
}
else if ( cmd.equals( "prepareupload" ) ) {
checkArgs( args, 5, false, "the VASL .vmod file, scenario file and 2 output files" ) ;
VassalShim shim = new VassalShim( args[1], null ) ;
shim.prepareUpload( args[2], args[3], args[4] ) ;
System.exit( 0 ) ;
}
else if ( cmd.equals( "version" ) ) {
checkArgs( args, 2, false, "the output file" ) ;
System.out.println( Info.getVersion() ) ;

@ -355,9 +355,9 @@ public class VassalShim
cmd.execute() ;
// extract the labels from the scenario
logger.info( "Searching the VASL scenario for labels (players={};{})...", players[0], players[1] ) ;
Map<String,GamePieceLabelFields> ourLabels = new HashMap<String,GamePieceLabelFields>() ;
ArrayList<GamePieceLabelFields> otherLabels = new ArrayList<GamePieceLabelFields>() ;
logger.info( "Searching the VASL scenario for labels (players={};{})...", players[0], players[1] ) ;
AppBoolean hasPlayerOwnedLabels = new AppBoolean( false ) ;
extractLabels( cmd, players, hasPlayerOwnedLabels, ourLabels, otherLabels, true ) ;
@ -496,9 +496,11 @@ public class VassalShim
String nat = snippetId.substring( 0, pos ) ;
if ( ! nat.equals( "extras" ) )
hasPlayerOwnedLabels.setVal( true ) ;
if ( ! nat.equals( players[0] ) && ! nat.equals( players[1] ) ) {
addSnippet = false ;
logger.debug( "- Skipping label: {} (owner={})", snippetId, nat ) ;
if ( players != null ) {
if ( ! nat.equals( players[0] ) && ! nat.equals( players[1] ) ) {
addSnippet = false ;
logger.debug( "- Skipping label: {} (owner={})", snippetId, nat ) ;
}
}
}
if ( addSnippet ) {
@ -919,6 +921,98 @@ public class VassalShim
Utils.saveXml( doc, reportFilename ) ;
}
public void takeScreenshot( String scenarioFilename, String outputFilename )
throws IOException, InterruptedException
{
// load the scenario
Command cmd = loadScenario( scenarioFilename ) ;
cmd.execute() ;
// generate the screenshot
doTakeScreenshot( cmd, outputFilename ) ;
}
private void doTakeScreenshot( Command cmd, String outputFilename )
throws IOException, InterruptedException
{
// figure out how big to make the screenshot
VASSAL.build.module.Map map = selectMap() ;
Dimension mapSize = map.mapSize() ;
map.getZoomer().setZoomFactor( 1.0 ) ;
int mapWidth = mapSize.width ;
int mapHeight = mapSize.height ;
// generate a screenshot
AppImageSaver imageSaver = new AppImageSaver( map ) ;
File outputFile = new File( outputFilename ) ;
int timeout = Integer.parseInt( config.getProperty( "SCREENSHOT_TIMEOUT", "30" ) ) ;
logger.debug( "Creating screenshot: width=" + mapWidth + " ; height=" + mapHeight + " ; timeout=" + timeout ) ;
imageSaver.generateScreenshot( outputFile, mapWidth, mapHeight, timeout ) ;
}
public void prepareUpload( String scenarioFilename, String strippedScenarioFilename, String screenshotFilename )
throws IOException, InterruptedException
{
// load the scenario
Command cmd = loadScenario( scenarioFilename ) ;
cmd.execute() ;
// figure out what labels we want to strip
logger.debug( "Configuring snippet ID's to strip:" ) ;
ArrayList<Pattern> snippetIdPatterns = new ArrayList<Pattern>() ;
String[] targetSnippetIds = config.getProperty( "STRIP_SNIPPETS_FOR_UPLOAD",
"victory_conditions ssr /ob_(vehicle|ordnance)_note_[0-9.]+"
).split( "\\s+" ) ;
for ( String regex: targetSnippetIds ) {
logger.debug( "- " + regex ) ;
snippetIdPatterns.add( Pattern.compile( regex ) ) ;
}
// extract the labels from the scenario
logger.debug( "Searching the VASL scenario for labels." ) ;
AppBoolean hasPlayerOwnedLabels = new AppBoolean( false ) ;
Map<String,GamePieceLabelFields> ourLabels = new HashMap<String,GamePieceLabelFields>() ;
ArrayList<GamePieceLabelFields> otherLabels = new ArrayList<GamePieceLabelFields>() ;
extractLabels( cmd, null, hasPlayerOwnedLabels, ourLabels, otherLabels, false ) ;
// process each label
logger.info( "Stripping labels..." ) ;
for ( String snippetId: ourLabels.keySet() ) {
// check if this is a label we want to strip
boolean rc = false ;
for ( Pattern pattern: snippetIdPatterns ) {
if ( pattern.matcher( snippetId ).find() ) {
rc = true ;
break ;
}
}
if ( ! rc ) {
// nope - leave it alone
logger.debug( "- Ignoring \"" + snippetId + "\"." ) ;
continue ;
}
// yup - make it so
logger.info( "- Stripping \"" + snippetId + "\"." ) ;
GamePieceLabelFields labelFields = ourLabels.get( snippetId ) ;
RemovePiece removeCmd = new RemovePiece( labelFields.gamePiece() ) ;
try {
removeCmd.execute() ;
} catch( Exception ex ) {
String msg = "ERROR: Couldn't delete label '" + snippetId + "'" ;
logger.warn( msg, ex ) ;
}
}
// save the scenario
saveScenario( strippedScenarioFilename ) ;
// generate the screenshot
// NOTE: This sometimes crashes (NPE), so it must be done last.
logger.info( "Generating screenshot..." ) ;
doTakeScreenshot( cmd, screenshotFilename ) ;
logger.info( "- OK." ) ;
}
private VASSAL.build.module.Map selectMap()
{
// NOTE: VASL 6.5.0 introduced a new map ("Casualties") as part of the new Casualties Bin feature,

Loading…
Cancel
Save