mirror of
https://github.com/Screenly/Anthias.git
synced 2025-12-23 22:38:05 -05:00
feat: migrate to React (#2265)
This commit is contained in:
@@ -16,12 +16,33 @@ indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 100
|
||||
|
||||
[*.{js,mjs,jsx}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
||||
|
||||
# CSS files
|
||||
[*.{css,scss,sass,less}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
||||
|
||||
[*.html]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 120
|
||||
|
||||
[package.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[**.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Python files
|
||||
[*.py]
|
||||
indent_style = space
|
||||
|
||||
7
.github/workflows/javascript-lint.yaml
vendored
7
.github/workflows/javascript-lint.yaml
vendored
@@ -1,11 +1,11 @@
|
||||
name: Run JavaScript and CoffeeScript Linter
|
||||
name: Run JavaScript Linter and Formatter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**/*.js'
|
||||
- '**/*.coffee'
|
||||
- '**/*.jsx'
|
||||
- '**/*.mjs'
|
||||
- '.github/workflows/javascript-lint.yaml'
|
||||
pull_request:
|
||||
@@ -33,3 +33,6 @@ jobs:
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run Prettier
|
||||
run: npm run format:check
|
||||
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
25
.prettierrc
Normal file
25
.prettierrc
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.js", "*.jsx", "*.mjs"],
|
||||
"options": {
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"semi": false,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.scss"],
|
||||
"options": {
|
||||
"singleQuote": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.css"],
|
||||
"options": {
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
0
anthias_app/management/__init__.py
Normal file
0
anthias_app/management/__init__.py
Normal file
0
anthias_app/management/commands/__init__.py
Normal file
0
anthias_app/management/commands/__init__.py
Normal file
100
anthias_app/management/commands/seed.py
Normal file
100
anthias_app/management/commands/seed.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from anthias_app.models import Asset
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seeds the database with sample web assets'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--clear',
|
||||
action='store_true',
|
||||
help='Clear existing assets before seeding',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['clear']:
|
||||
self.stdout.write('Clearing existing assets...')
|
||||
Asset.objects.all().delete()
|
||||
|
||||
self.stdout.write('Creating sample web assets...')
|
||||
|
||||
# Create some sample web assets
|
||||
assets = [
|
||||
{
|
||||
'name': 'Google Homepage',
|
||||
'uri': 'https://www.google.com',
|
||||
'mimetype': 'text/html',
|
||||
'is_enabled': True,
|
||||
'start_date': timezone.now(),
|
||||
'end_date': timezone.now() + timedelta(days=7),
|
||||
'play_order': 0,
|
||||
'duration': 10,
|
||||
},
|
||||
{
|
||||
'name': 'GitHub Homepage',
|
||||
'uri': 'https://github.com',
|
||||
'mimetype': 'text/html',
|
||||
'is_enabled': True,
|
||||
'start_date': timezone.now(),
|
||||
'end_date': timezone.now() + timedelta(days=14),
|
||||
'play_order': 1,
|
||||
'duration': 10,
|
||||
},
|
||||
{
|
||||
'name': 'Django Documentation',
|
||||
'uri': 'https://docs.djangoproject.com',
|
||||
'mimetype': 'text/html',
|
||||
'is_enabled': True,
|
||||
'start_date': timezone.now() - timedelta(days=1),
|
||||
'end_date': timezone.now() + timedelta(days=21),
|
||||
'play_order': 2,
|
||||
'duration': 10,
|
||||
},
|
||||
{
|
||||
'name': 'Vue.js Homepage',
|
||||
'uri': 'https://vuejs.org',
|
||||
'mimetype': 'text/html',
|
||||
'is_enabled': True,
|
||||
'start_date': timezone.now() - timedelta(days=1),
|
||||
'end_date': timezone.now() + timedelta(days=21),
|
||||
'play_order': 3,
|
||||
'duration': 10,
|
||||
},
|
||||
{
|
||||
'name': 'React Homepage',
|
||||
'uri': 'https://reactjs.org',
|
||||
'mimetype': 'text/html',
|
||||
'is_enabled': True,
|
||||
'start_date': timezone.now() - timedelta(days=1),
|
||||
'end_date': timezone.now() + timedelta(days=21),
|
||||
'play_order': 4,
|
||||
'duration': 10,
|
||||
},
|
||||
{
|
||||
'name': 'Angular Homepage',
|
||||
'uri': 'https://angular.io',
|
||||
'mimetype': 'text/html',
|
||||
'is_enabled': True,
|
||||
'start_date': timezone.now() - timedelta(days=1),
|
||||
'end_date': timezone.now() + timedelta(days=21),
|
||||
'play_order': 5,
|
||||
'duration': 10,
|
||||
}
|
||||
]
|
||||
|
||||
for asset_data in assets:
|
||||
Asset.objects.create(**asset_data)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'Created web asset: {asset_data["name"]}')
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
'Successfully seeded the database with web assets'
|
||||
)
|
||||
)
|
||||
@@ -1,13 +1,10 @@
|
||||
from django.urls import path
|
||||
from django.urls import path, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'anthias_app'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('settings', views.settings_page, name='settings'),
|
||||
path('system-info', views.system_info, name='system_info'),
|
||||
path('integrations', views.integrations, name='integrations'),
|
||||
path('splash-page', views.splash_page, name='splash_page'),
|
||||
re_path(r'^(?!api/).*$', views.react, name='react'),
|
||||
]
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
import ipaddress
|
||||
from datetime import timedelta
|
||||
from os import (
|
||||
getenv,
|
||||
statvfs,
|
||||
)
|
||||
from platform import machine
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import psutil
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from hurry.filesize import size
|
||||
|
||||
from lib import (
|
||||
device_helper,
|
||||
diagnostics,
|
||||
)
|
||||
from lib.auth import authorized
|
||||
from lib.utils import (
|
||||
connect_to_redis,
|
||||
get_node_ip,
|
||||
get_node_mac_address,
|
||||
is_balena_app,
|
||||
is_demo_node,
|
||||
is_docker,
|
||||
)
|
||||
from settings import (
|
||||
CONFIGURABLE_SETTINGS,
|
||||
DEFAULTS,
|
||||
ZmqPublisher,
|
||||
settings,
|
||||
)
|
||||
|
||||
from .helpers import (
|
||||
add_default_assets,
|
||||
remove_default_assets,
|
||||
template,
|
||||
)
|
||||
|
||||
@@ -41,229 +16,8 @@ r = connect_to_redis()
|
||||
|
||||
|
||||
@authorized
|
||||
@require_http_methods(["GET"])
|
||||
def index(request):
|
||||
player_name = settings['player_name']
|
||||
my_ip = urlparse(request.build_absolute_uri()).hostname
|
||||
is_demo = is_demo_node()
|
||||
balena_device_uuid = getenv("BALENA_DEVICE_UUID", None)
|
||||
|
||||
ws_addresses = []
|
||||
|
||||
if settings['use_ssl']:
|
||||
ws_addresses.append('wss://' + my_ip + '/ws/')
|
||||
else:
|
||||
ws_addresses.append('ws://' + my_ip + '/ws/')
|
||||
|
||||
if balena_device_uuid:
|
||||
ws_addresses.append(
|
||||
'wss://{}.balena-devices.com/ws/'.format(balena_device_uuid)
|
||||
)
|
||||
|
||||
return template(request, 'index.html', {
|
||||
'ws_addresses': ws_addresses,
|
||||
'player_name': player_name,
|
||||
'is_demo': is_demo,
|
||||
'is_balena': is_balena_app(),
|
||||
})
|
||||
|
||||
|
||||
@authorized
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def settings_page(request):
|
||||
context = {'flash': None}
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
current_pass = request.POST.get('current-password', '')
|
||||
auth_backend = request.POST.get('auth_backend', '')
|
||||
|
||||
if (
|
||||
auth_backend != settings['auth_backend']
|
||||
and settings['auth_backend']
|
||||
):
|
||||
if not current_pass:
|
||||
raise ValueError(
|
||||
"Must supply current password to change "
|
||||
"authentication method"
|
||||
)
|
||||
if not settings.auth.check_password(current_pass):
|
||||
raise ValueError("Incorrect current password.")
|
||||
|
||||
prev_auth_backend = settings['auth_backend']
|
||||
if not current_pass and prev_auth_backend:
|
||||
current_pass_correct = None
|
||||
else:
|
||||
current_pass_correct = (
|
||||
settings
|
||||
.auth_backends[prev_auth_backend]
|
||||
.check_password(current_pass)
|
||||
)
|
||||
next_auth_backend = settings.auth_backends[auth_backend]
|
||||
next_auth_backend.update_settings(request, current_pass_correct)
|
||||
settings['auth_backend'] = auth_backend
|
||||
|
||||
for field, default in list(CONFIGURABLE_SETTINGS.items()):
|
||||
value = request.POST.get(field, default)
|
||||
|
||||
if (
|
||||
not value
|
||||
and field in [
|
||||
'default_duration',
|
||||
'default_streaming_duration',
|
||||
]
|
||||
):
|
||||
value = str(0)
|
||||
if isinstance(default, bool):
|
||||
value = value == 'on'
|
||||
|
||||
if field == 'default_assets' and settings[field] != value:
|
||||
if value:
|
||||
add_default_assets()
|
||||
else:
|
||||
remove_default_assets()
|
||||
|
||||
settings[field] = value
|
||||
|
||||
settings.save()
|
||||
publisher = ZmqPublisher.get_instance()
|
||||
publisher.send_to_viewer('reload')
|
||||
context['flash'] = {
|
||||
'class': "success",
|
||||
'message': "Settings were successfully saved.",
|
||||
}
|
||||
except ValueError as e:
|
||||
context['flash'] = {'class': "danger", 'message': e}
|
||||
except IOError as e:
|
||||
context['flash'] = {'class': "danger", 'message': e}
|
||||
except OSError as e:
|
||||
context['flash'] = {'class': "danger", 'message': e}
|
||||
else:
|
||||
settings.load()
|
||||
for field, _default in list(DEFAULTS['viewer'].items()):
|
||||
context[field] = settings[field]
|
||||
|
||||
auth_backends = []
|
||||
for backend in settings.auth_backends_list:
|
||||
if backend.template:
|
||||
html, ctx = backend.template
|
||||
context.update(ctx)
|
||||
else:
|
||||
html = None
|
||||
auth_backends.append({
|
||||
'name': backend.name,
|
||||
'text': backend.display_name,
|
||||
'template': html,
|
||||
'selected': (
|
||||
'selected'
|
||||
if settings['auth_backend'] == backend.name
|
||||
else ''
|
||||
),
|
||||
})
|
||||
|
||||
ip_addresses = get_node_ip().split()
|
||||
|
||||
context.update({
|
||||
'user': settings['user'],
|
||||
'need_current_password': bool(settings['auth_backend']),
|
||||
'is_balena': is_balena_app(),
|
||||
'is_docker': is_docker(),
|
||||
'auth_backend': settings['auth_backend'],
|
||||
'auth_backends': auth_backends,
|
||||
'ip_addresses': ip_addresses,
|
||||
'host_user': getenv('HOST_USER'),
|
||||
'device_type': getenv('DEVICE_TYPE')
|
||||
})
|
||||
|
||||
return template(request, 'settings.html', context)
|
||||
|
||||
|
||||
@authorized
|
||||
@require_http_methods(["GET"])
|
||||
def system_info(request):
|
||||
loadavg = diagnostics.get_load_avg()['15 min']
|
||||
display_power = r.get('display_power')
|
||||
|
||||
# Calculate disk space
|
||||
slash = statvfs("/")
|
||||
free_space = size(slash.f_bavail * slash.f_frsize)
|
||||
|
||||
# Memory
|
||||
virtual_memory = psutil.virtual_memory()
|
||||
memory = {
|
||||
'total': virtual_memory.total >> 20,
|
||||
'used': virtual_memory.used >> 20,
|
||||
'free': virtual_memory.free >> 20,
|
||||
'shared': virtual_memory.shared >> 20,
|
||||
'buff': virtual_memory.buffers >> 20,
|
||||
'available': virtual_memory.available >> 20
|
||||
}
|
||||
|
||||
# Get uptime
|
||||
system_uptime = timedelta(seconds=diagnostics.get_uptime())
|
||||
|
||||
# Player name for title
|
||||
player_name = settings['player_name']
|
||||
|
||||
device_model = device_helper.parse_cpu_info().get('model')
|
||||
|
||||
if device_model is None and machine() == 'x86_64':
|
||||
device_model = 'Generic x86_64 Device'
|
||||
|
||||
git_branch = diagnostics.get_git_branch()
|
||||
git_short_hash = diagnostics.get_git_short_hash()
|
||||
anthias_commit_link = None
|
||||
|
||||
if git_branch == 'master':
|
||||
anthias_commit_link = (
|
||||
'https://github.com/Screenly/Anthias'
|
||||
f'/commit/{git_short_hash}'
|
||||
)
|
||||
|
||||
anthias_version = '{}@{}'.format(
|
||||
git_branch,
|
||||
git_short_hash,
|
||||
)
|
||||
|
||||
context = {
|
||||
'player_name': player_name,
|
||||
'loadavg': loadavg,
|
||||
'free_space': free_space,
|
||||
'uptime': {
|
||||
'days': system_uptime.days,
|
||||
'hours': round(system_uptime.seconds / 3600, 2),
|
||||
},
|
||||
'memory': memory,
|
||||
'display_power': display_power,
|
||||
'device_model': device_model,
|
||||
'anthias_version': anthias_version,
|
||||
'anthias_commit_link': anthias_commit_link,
|
||||
'mac_address': get_node_mac_address(),
|
||||
'is_balena': is_balena_app(),
|
||||
}
|
||||
|
||||
return template(request, 'system-info.html', context)
|
||||
|
||||
|
||||
@authorized
|
||||
@require_http_methods(["GET"])
|
||||
def integrations(request):
|
||||
context = {
|
||||
'player_name': settings['player_name'],
|
||||
'is_balena': is_balena_app(),
|
||||
}
|
||||
|
||||
if context['is_balena']:
|
||||
context.update({
|
||||
'balena_device_id': getenv('BALENA_DEVICE_UUID'),
|
||||
'balena_app_id': getenv('BALENA_APP_ID'),
|
||||
'balena_app_name': getenv('BALENA_APP_NAME'),
|
||||
'balena_supervisor_version': getenv('BALENA_SUPERVISOR_VERSION'),
|
||||
'balena_host_os_version': getenv('BALENA_HOST_OS_VERSION'),
|
||||
'balena_device_name_at_init': getenv('BALENA_DEVICE_NAME_AT_INIT'),
|
||||
})
|
||||
|
||||
return template(request, 'integrations.html', context)
|
||||
def react(request):
|
||||
return template(request, 'react.html', {})
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
|
||||
@@ -16,8 +16,8 @@ COPY package.json \
|
||||
/app/
|
||||
RUN npm install
|
||||
|
||||
COPY ./static/js/*.coffee /app/static/js/
|
||||
COPY ./static/sass/*.scss /app/static/sass/
|
||||
COPY ./static/src/ /app/static/src/
|
||||
RUN npm run build
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -123,7 +123,7 @@ $ docker compose -f docker-compose.dev.yml exec anthias-server \
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Making changes to the CoffeeScript or SCSS files will automatically trigger a recompilation,
|
||||
Making changes to the JavaScript, JSX, or SCSS files will automatically trigger a recompilation,
|
||||
generating the corresponding JavaScript and CSS files.
|
||||
|
||||
### Closing the transpiler
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
@@ -23,14 +25,40 @@ export default [
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
react: reactPlugin
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'semi': ['error', 'always'],
|
||||
'semi': ['error', 'never'],
|
||||
'quotes': ['error', 'single'],
|
||||
'indent': ['error', 2],
|
||||
'no-unused-vars': 'warn',
|
||||
'no-console': 'warn',
|
||||
'no-debugger': 'warn'
|
||||
'no-unused-vars': 'error',
|
||||
'no-console': 'error',
|
||||
'no-debugger': 'warn',
|
||||
'react/jsx-uses-react': 'error',
|
||||
'react/jsx-uses-vars': 'error',
|
||||
'react/jsx-no-duplicate-props': 'error',
|
||||
'react/jsx-key': 'warn',
|
||||
'react/jsx-max-props-per-line': ['warn', { maximum: 1, when: 'multiline' }],
|
||||
'react/jsx-first-prop-new-line': ['warn', 'multiline'],
|
||||
'react/jsx-closing-bracket-location': ['warn', 'line-aligned'],
|
||||
'react/jsx-tag-spacing': ['warn', {
|
||||
closingSlash: 'never',
|
||||
beforeSelfClosing: 'always',
|
||||
afterOpening: 'never',
|
||||
beforeClosing: 'never'
|
||||
}]
|
||||
}
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
3684
package-lock.json
generated
3684
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -4,24 +4,42 @@
|
||||
"scripts": {
|
||||
"dev": "webpack --watch --config webpack.dev.js",
|
||||
"build": "webpack --config webpack.prod.js",
|
||||
"lint": "coffeelint static/js/*.coffee && eslint ."
|
||||
"lint": "eslint static/src",
|
||||
"format:check": "prettier --check static/src",
|
||||
"format:fix": "prettier --write static/src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"bootstrap": "^4.3.1",
|
||||
"css-toggle-switch": "^4.1.0"
|
||||
"css-toggle-switch": "^4.1.0",
|
||||
"jquery": "^3.7.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-router": "^7.0.2",
|
||||
"sweetalert2": "^11.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coffeelint/cli": "^5.2.11",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"babel-loader": "^9.2.1",
|
||||
"classnames": "^2.5.1",
|
||||
"coffee-loader": "^0.9.0",
|
||||
"coffeescript": "^1.12.7",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"prettier": "3.5.3",
|
||||
"sass": "^1.75.0",
|
||||
"sass-loader": "^16.0.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-merge": "^6.0.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.33.2"
|
||||
"webpack-merge": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,676 +0,0 @@
|
||||
### anthias ui ###
|
||||
|
||||
import '../sass/anthias.scss'
|
||||
|
||||
$().ready ->
|
||||
$('#subsribe-form-container').popover content: get_template 'subscribe-form'
|
||||
|
||||
|
||||
API = (window.Anthias ||= {}) # exports
|
||||
|
||||
dateSettings = {}
|
||||
|
||||
if use24HourClock
|
||||
dateSettings.time = "HH:mm"
|
||||
dateSettings.fullTime = "HH:mm:ss"
|
||||
dateSettings.showMeridian = false
|
||||
else
|
||||
dateSettings.time = "hh:mm A"
|
||||
dateSettings.fullTime = "hh:mm:ss A"
|
||||
dateSettings.showMeridian = true
|
||||
|
||||
dateSettings.date = dateFormat.toUpperCase()
|
||||
dateSettings.datepickerFormat = dateFormat
|
||||
|
||||
dateSettings.fullDate = "#{dateSettings.date} #{dateSettings.fullTime}"
|
||||
|
||||
|
||||
API.date_to = date_to = (d) ->
|
||||
# Cross-browser UTC to localtime conversion
|
||||
dd = moment.utc(d).local()
|
||||
string: -> dd.format dateSettings.fullDate
|
||||
date: -> dd.format dateSettings.date
|
||||
time: -> dd.format dateSettings.time
|
||||
|
||||
now = -> new Date()
|
||||
|
||||
get_template = (name) -> _.template ($ "##{name}-template").html()
|
||||
delay = (wait, fn) -> _.delay fn, wait
|
||||
|
||||
mimetypes = [ [('jpe jpg jpeg png pnm gif bmp'.split ' '), 'image']
|
||||
[('avi mkv mov mpg mpeg mp4 ts flv'.split ' '), 'video']]
|
||||
viduris = ('rtsp rtmp'.split ' ')
|
||||
domains = [ [('www.youtube.com youtu.be'.split ' '), 'youtube_asset']]
|
||||
|
||||
|
||||
getMimetype = (filename) ->
|
||||
scheme = (_.first filename.split ':').toLowerCase()
|
||||
match = scheme in viduris
|
||||
if match
|
||||
return 'streaming'
|
||||
|
||||
domain = (_.first ((_.last filename.split '//').toLowerCase()).split '/')
|
||||
mt = _.find domains, (mt) -> domain in mt[0]
|
||||
if mt and domain in mt[0]
|
||||
return mt[1]
|
||||
|
||||
ext = (_.last filename.split '.').toLowerCase()
|
||||
mt = _.find mimetypes, (mt) -> ext in mt[0]
|
||||
if mt
|
||||
return mt[1]
|
||||
|
||||
durationSecondsToHumanReadable = (secs) ->
|
||||
durationString = ""
|
||||
secInt = parseInt(secs)
|
||||
|
||||
if ((hours = Math.floor(secInt / 3600)) > 0)
|
||||
durationString += hours + " hours "
|
||||
if ((minutes = Math.floor(secInt / 60) % 60) > 0)
|
||||
durationString += minutes + " min "
|
||||
if ((seconds = (secInt % 60)) > 0)
|
||||
durationString += seconds + " sec"
|
||||
|
||||
return durationString
|
||||
|
||||
url_test = (v) ->
|
||||
urlPattern = /(http|https|rtsp|rtmp):\/\/[\w-]+(\.?[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/
|
||||
urlPattern.test v
|
||||
|
||||
get_filename = (v) -> (v.replace /[\/\\\s]+$/g, '').replace /^.*[\\\/]/g, ''
|
||||
truncate_str = (v) -> v.replace /(.{100})..+/, "$1..."
|
||||
insertWbr = (v) -> (v.replace /\//g, '/<wbr>').replace /\&/g, '&<wbr>'
|
||||
|
||||
# Tell Backbone to send its saves as JSON-encoded.
|
||||
Backbone.emulateJSON = off
|
||||
|
||||
# Models
|
||||
API.Asset = class Asset extends Backbone.Model
|
||||
idAttribute: "asset_id"
|
||||
fields: 'name mimetype uri start_date end_date duration skip_asset_check'.split ' '
|
||||
defaults: ->
|
||||
name: ''
|
||||
mimetype: 'webpage'
|
||||
uri: ''
|
||||
is_active: 1
|
||||
start_date: ''
|
||||
end_date: ''
|
||||
duration: defaultDuration
|
||||
is_enabled: 0
|
||||
is_processing: 0
|
||||
nocache: 0
|
||||
play_order: 0
|
||||
skip_asset_check: 0
|
||||
active: =>
|
||||
if @get('is_enabled') and @get('start_date') and @get('end_date')
|
||||
at = now()
|
||||
start_date = new Date(@get('start_date'))
|
||||
end_date = new Date(@get('end_date'))
|
||||
return start_date <= at <= end_date
|
||||
else
|
||||
return false
|
||||
|
||||
backup: =>
|
||||
@backup_attributes = @toJSON()
|
||||
|
||||
rollback: =>
|
||||
if @backup_attributes
|
||||
@set @backup_attributes
|
||||
@backup_attributes = undefined
|
||||
old_name: =>
|
||||
if @backup_attributes
|
||||
return @backup_attributes.name
|
||||
|
||||
|
||||
API.Assets = class Assets extends Backbone.Collection
|
||||
url: "/api/v2/assets"
|
||||
model: Asset
|
||||
comparator: 'play_order'
|
||||
|
||||
|
||||
# Views
|
||||
API.View = {}
|
||||
|
||||
API.View.AddAssetView = class AddAssetView extends Backbone.View
|
||||
$f: (field) => @$ "[name='#{field}']" # get field element
|
||||
$fv: (field, val...) => (@$f field).val val... # get or set filed value
|
||||
|
||||
initialize: (oprions) =>
|
||||
($ 'body').append @$el.html get_template 'asset-modal'
|
||||
(@$el.children ":first").modal()
|
||||
(@$ '.cancel').val 'Back to Assets'
|
||||
|
||||
deadlines = start: now(), end: (moment().add 'days', 30).toDate()
|
||||
for own tag, deadline of deadlines
|
||||
d = date_to deadline
|
||||
@.$fv "#{tag}_date_date", d.date()
|
||||
@.$fv "#{tag}_date_time", d.time()
|
||||
|
||||
no
|
||||
|
||||
viewmodel:(model) =>
|
||||
for which in ['start', 'end']
|
||||
date = (@$fv "#{which}_date_date") + " " + (@$fv "#{which}_date_time")
|
||||
@$fv "#{which}_date", (moment date, dateSettings.fullDate).toDate().toISOString()
|
||||
for field in model.fields when not (@$f field).prop 'disabled'
|
||||
model.set field, (@$fv field), silent:yes
|
||||
|
||||
events:
|
||||
'change': 'change'
|
||||
'click #save-asset': 'save'
|
||||
'click .cancel': 'cancel'
|
||||
'hidden.bs.modal': 'destroyFileUploadWidget'
|
||||
'click .tabnav-uri': 'clickTabNavUri'
|
||||
'click .tabnav-file_upload': 'clickTabNavUpload'
|
||||
'change .is_enabled-skip_asset_check_checkbox': 'toggleSkipAssetCheck'
|
||||
'keyup [name=uri]': 'change'
|
||||
|
||||
save: (e) =>
|
||||
if ((@$fv 'uri') == '')
|
||||
return no
|
||||
if (@$ '#tab-uri').hasClass 'active'
|
||||
model = new Asset {}, {collection: API.assets}
|
||||
@$fv 'mimetype', ''
|
||||
@updateUriMimetype()
|
||||
@viewmodel model
|
||||
model.set {name: model.get 'uri'}, silent:yes
|
||||
save = model.save()
|
||||
|
||||
(@$ 'input').prop 'disabled', on
|
||||
save.done (data) =>
|
||||
model.id = data.asset_id
|
||||
(@$el.children ":first").modal 'hide'
|
||||
_.extend model.attributes, data
|
||||
model.collection.add model
|
||||
save.fail =>
|
||||
(@$ 'input').prop 'disabled', off
|
||||
model.destroy()
|
||||
no
|
||||
|
||||
toggleSkipAssetCheck: (e) =>
|
||||
@$fv 'skip_asset_check', if parseInt((@$fv 'skip_asset_check')) == 1 then 0 else 1
|
||||
|
||||
change_mimetype: =>
|
||||
if (@$fv 'mimetype') == "video"
|
||||
@$fv 'duration', 0
|
||||
else if (@$fv 'mimetype') == "streaming"
|
||||
@$fv 'duration', defaultStreamingDuration
|
||||
else
|
||||
@$fv 'duration', defaultDuration
|
||||
|
||||
clickTabNavUpload: (e) =>
|
||||
if not (@$ '#tab-file_upload').hasClass 'active'
|
||||
(@$ 'ul.nav-tabs li').removeClass 'active show'
|
||||
(@$ '.tab-pane').removeClass 'active'
|
||||
(@$ '.tabnav-file_upload').addClass 'active show'
|
||||
(@$ '#tab-file_upload').addClass 'active'
|
||||
(@$ '.uri').hide()
|
||||
(@$ '.skip_asset_check_checkbox').hide()
|
||||
(@$ '#save-asset').hide()
|
||||
that = this
|
||||
(@$ "[name='file_upload']").fileupload
|
||||
autoUpload: false
|
||||
sequentialUploads: true
|
||||
maxChunkSize: 5000000 #5 MB
|
||||
url: 'api/v2/file_asset'
|
||||
progressall: (e, data) => if data.loaded and data.total
|
||||
(@$ '.progress .bar').css 'width', "#{data.loaded / data.total * 100}%"
|
||||
add: (e, data) ->
|
||||
(that.$ '.status').hide()
|
||||
(that.$ '.progress').show()
|
||||
|
||||
model = new Asset {}, {collection: API.assets}
|
||||
filename = data['files'][0]['name']
|
||||
that.$fv 'name', filename
|
||||
that.updateFileUploadMimetype(filename)
|
||||
that.viewmodel model
|
||||
|
||||
data.submit()
|
||||
.success (data) ->
|
||||
model.set {uri: data.uri, ext: data.ext}, silent:yes
|
||||
|
||||
save = model.save()
|
||||
save.done (data) ->
|
||||
model.id = data.asset_id
|
||||
_.extend model.attributes, data
|
||||
model.collection.add model
|
||||
save.fail ->
|
||||
model.destroy()
|
||||
.error ->
|
||||
model.destroy()
|
||||
stop: (e) ->
|
||||
(that.$ '.progress').hide()
|
||||
(that.$ '.progress .bar').css 'width', "0"
|
||||
done: (e, data) ->
|
||||
(that.$ '.status').show()
|
||||
(that.$ '.status').html 'Upload completed.'
|
||||
setTimeout ->
|
||||
(that.$ '.status').fadeOut('slow')
|
||||
, 5000
|
||||
no
|
||||
|
||||
clickTabNavUri: (e) => # TODO: clean
|
||||
if not (@$ '#tab-uri').hasClass 'active'
|
||||
(@$ "[name='file_upload']").fileupload 'destroy'
|
||||
(@$ 'ul.nav-tabs li').removeClass 'active show'
|
||||
(@$ '.tab-pane').removeClass 'active'
|
||||
(@$ '.tabnav-uri').addClass 'active show'
|
||||
(@$ '#tab-uri').addClass 'active'
|
||||
(@$ '#save-asset').show()
|
||||
(@$ '.uri').show()
|
||||
(@$ '.skip_asset_check_checkbox').show()
|
||||
(@$ '.status').hide()
|
||||
(@$f 'uri').focus()
|
||||
|
||||
updateUriMimetype: => @updateMimetype @$fv 'uri'
|
||||
updateFileUploadMimetype: (filename) => @updateMimetype filename
|
||||
updateMimetype: (filename) =>
|
||||
mt = getMimetype filename
|
||||
@$fv 'mimetype', if mt then mt else new Asset().defaults()['mimetype']
|
||||
@change_mimetype()
|
||||
|
||||
change: (e) =>
|
||||
@_change ||= _.throttle (=>
|
||||
@validate()
|
||||
yes), 500
|
||||
@_change arguments...
|
||||
|
||||
validate: (e) =>
|
||||
that = this
|
||||
validators =
|
||||
uri: (v) ->
|
||||
if v
|
||||
if ((that.$ '#tab-uri').hasClass 'active') and not url_test v
|
||||
'please enter a valid URL'
|
||||
errors = ([field, v] for field, fn of validators when v = fn (@$fv field))
|
||||
|
||||
(@$ ".form-group .help-inline.invalid-feedback").remove()
|
||||
(@$ ".form-group .form-control").removeClass 'is-invalid'
|
||||
(@$ '[type=submit]').prop 'disabled', no
|
||||
for [field, v] in errors
|
||||
(@$ '[type=submit]').prop 'disabled', yes
|
||||
(@$ ".form-group.#{field} .form-control").addClass 'is-invalid'
|
||||
(@$ ".form-group.#{field} .controls").append \
|
||||
$ ("<span class='help-inline invalid-feedback'>#{v}</span>")
|
||||
|
||||
cancel: (e) =>
|
||||
(@$el.children ":first").modal 'hide'
|
||||
|
||||
destroyFileUploadWidget: (e) =>
|
||||
if (@$ '#tab-file_upload').hasClass 'active'
|
||||
(@$ "[name='file_upload']").fileupload 'destroy'
|
||||
|
||||
|
||||
API.View.EditAssetView = class EditAssetView extends Backbone.View
|
||||
$f: (field) => @$ "[name='#{field}']" # get field element
|
||||
$fv: (field, val...) => (@$f field).val val... # get or set filed value
|
||||
|
||||
initialize: (options) =>
|
||||
($ 'body').append @$el.html get_template 'asset-modal'
|
||||
(@$ 'input.time').timepicker
|
||||
minuteStep: 5, showInputs: yes, disableFocus: yes, showMeridian: dateSettings.showMeridian
|
||||
|
||||
(@$ 'input[name="nocache"]').prop 'checked', @model.get 'nocache'
|
||||
(@$ '.modal-header .close').remove()
|
||||
(@$el.children ":first").modal()
|
||||
|
||||
@model.backup()
|
||||
|
||||
@model.bind 'change', @render
|
||||
|
||||
@render()
|
||||
@validate()
|
||||
no
|
||||
|
||||
render: () =>
|
||||
@undelegateEvents()
|
||||
(@$ f).attr 'disabled', on for f in 'mimetype uri file_upload'.split ' '
|
||||
(@$ '#modalLabel').text "Edit Asset"
|
||||
(@$ '.asset-location').hide(); (@$ '.uri').hide(); (@$ '.skip_asset_check_checkbox').hide()
|
||||
(@$ '.asset-location.edit').show()
|
||||
(@$ '.mime-select').prop('disabled', 'true')
|
||||
|
||||
if (@model.get 'mimetype') == 'video'
|
||||
(@$f 'duration').prop 'disabled', on
|
||||
|
||||
for field in @model.fields
|
||||
if (@$fv field) != @model.get field
|
||||
@$fv field, @model.get field
|
||||
(@$ '.uri-text').html insertWbr truncate_str (@model.get 'uri')
|
||||
|
||||
for which in ['start', 'end']
|
||||
d = date_to @model.get "#{which}_date"
|
||||
@$fv "#{which}_date_date", d.date()
|
||||
(@$f "#{which}_date_date").datepicker autoclose: yes, format: dateSettings.datepickerFormat
|
||||
(@$f "#{which}_date_date").datepicker 'setValue', d.date()
|
||||
@$fv "#{which}_date_time", d.time()
|
||||
|
||||
@displayAdvanced()
|
||||
@delegateEvents()
|
||||
no
|
||||
|
||||
viewmodel: =>
|
||||
for which in ['start', 'end']
|
||||
date = (@$fv "#{which}_date_date") + " " + (@$fv "#{which}_date_time")
|
||||
@$fv "#{which}_date", (moment date, dateSettings.fullDate).toDate().toISOString()
|
||||
for field in @model.fields when not (@$f field).prop 'disabled'
|
||||
@model.set field, (@$fv field), silent:yes
|
||||
|
||||
events:
|
||||
'click #save-asset': 'save'
|
||||
'click .cancel': 'cancel'
|
||||
'change': 'change'
|
||||
'keyup': 'change'
|
||||
'click .advanced-toggle': 'toggleAdvanced'
|
||||
|
||||
changeLoopTimes: =>
|
||||
current_date = new Date()
|
||||
end_date = new Date()
|
||||
|
||||
switch @$('#loop_times').val()
|
||||
when "day"
|
||||
@setLoopDateTime (date_to current_date), (date_to end_date.setDate(current_date.getDate() + 1))
|
||||
when "week"
|
||||
@setLoopDateTime (date_to current_date), (date_to end_date.setDate(current_date.getDate() + 7))
|
||||
when "month"
|
||||
@setLoopDateTime (date_to current_date), (date_to end_date.setMonth(current_date.getMonth() + 1))
|
||||
when "year"
|
||||
@setLoopDateTime (date_to current_date), (date_to end_date.setFullYear(current_date.getFullYear() + 1))
|
||||
when "forever"
|
||||
@setLoopDateTime (date_to current_date), (date_to end_date.setFullYear(9999))
|
||||
when "manual"
|
||||
@setDisabledDatepicker(false)
|
||||
(@$ "#manul_date").show()
|
||||
return
|
||||
else
|
||||
return
|
||||
@setDisabledDatepicker(true)
|
||||
(@$ "#manul_date").hide()
|
||||
|
||||
save: (e) =>
|
||||
@viewmodel()
|
||||
save = null
|
||||
@model.set 'nocache', if (@$ 'input[name="nocache"]').prop 'checked' then 1 else 0
|
||||
|
||||
if not @model.get 'name'
|
||||
if @model.old_name()
|
||||
@model.set {name: @model.old_name()}, silent:yes
|
||||
else if getMimetype @model.get 'uri'
|
||||
@model.set {name: get_filename @model.get 'uri'}, silent:yes
|
||||
else
|
||||
@model.set {name: @model.get 'uri'}, silent:yes
|
||||
save = @model.save()
|
||||
|
||||
(@$ 'input, select').prop 'disabled', on
|
||||
save.done (data) =>
|
||||
@model.id = data.asset_id
|
||||
@collection.add @model if not @model.collection
|
||||
(@$el.children ":first").modal 'hide'
|
||||
_.extend @model.attributes, data
|
||||
save.fail =>
|
||||
(@$ '.progress').hide()
|
||||
(@$ 'input, select').prop 'disabled', off
|
||||
no
|
||||
|
||||
change: (e) =>
|
||||
@_change ||= _.throttle (=>
|
||||
@changeLoopTimes()
|
||||
@viewmodel()
|
||||
@model.trigger 'change'
|
||||
@validate(e)
|
||||
yes), 500
|
||||
@_change arguments...
|
||||
|
||||
validate: (e) =>
|
||||
that = this
|
||||
validators =
|
||||
duration: (v) =>
|
||||
if ('video' isnt @model.get 'mimetype') and (not (_.isNumber v*1 ) or v*1 < 1)
|
||||
'Please enter a valid number.'
|
||||
end_date: (v) =>
|
||||
unless (new Date @$fv 'start_date') < (new Date @$fv 'end_date')
|
||||
if $(e?.target)?.attr("name") == "start_date_date"
|
||||
start_date = new Date @$fv 'start_date'
|
||||
end_date = new Date(start_date.getTime() + Math.max(parseInt(@$fv 'duration'), 60) * 1000)
|
||||
@setLoopDateTime (date_to start_date), (date_to end_date)
|
||||
return
|
||||
|
||||
'End date should be after start date.'
|
||||
errors = ([field, v] for field, fn of validators when v = fn (@$fv field))
|
||||
|
||||
(@$ ".form-group .help-inline.invalid-feedback").remove()
|
||||
(@$ ".form-group .form-control").removeClass 'is-invalid'
|
||||
(@$ '[type=submit]').prop 'disabled', no
|
||||
for [field, v] in errors
|
||||
(@$ '[type=submit]').prop 'disabled', yes
|
||||
(@$ ".form-group.#{field} .form-control").addClass 'is-invalid'
|
||||
(@$ ".form-group.#{field} .controls").append \
|
||||
$ ("<span class='help-inline invalid-feedback'>#{v}</span>")
|
||||
|
||||
|
||||
cancel: (e) =>
|
||||
@model.rollback()
|
||||
(@$el.children ":first").modal 'hide'
|
||||
|
||||
toggleAdvanced: =>
|
||||
(@$ '.fa-play').toggleClass 'rotated'
|
||||
(@$ '.fa-play').toggleClass 'unrotated'
|
||||
(@$ '.collapse-advanced').collapse 'toggle'
|
||||
|
||||
displayAdvanced: =>
|
||||
img = 'image' is @$fv 'mimetype'
|
||||
edit = url_test @model.get 'uri'
|
||||
has_nocache = img and edit
|
||||
(@$ '.advanced-accordion').toggle has_nocache is on
|
||||
|
||||
setLoopDateTime: (start_date, end_date) =>
|
||||
@$fv "start_date_date", start_date.date()
|
||||
(@$f "start_date_date").datepicker autoclose: yes, format: dateSettings.datepickerFormat
|
||||
(@$f "start_date_date").datepicker 'setDate', moment(start_date.date(), dateSettings.date).toDate()
|
||||
@$fv "start_date_time", start_date.time()
|
||||
@$fv "end_date_date", end_date.date()
|
||||
(@$f "end_date_date").datepicker autoclose: yes, format: dateSettings.datepickerFormat
|
||||
(@$f "end_date_date").datepicker 'setDate', moment(end_date.date(), dateSettings.date).toDate()
|
||||
@$fv "end_date_time", end_date.time()
|
||||
|
||||
(@$ ".form-group .help-inline.invalid-feedback").remove()
|
||||
(@$ ".form-group .form-control").removeClass 'is-invalid'
|
||||
(@$ '[type=submit]').prop 'disabled', no
|
||||
|
||||
setDisabledDatepicker: (b) =>
|
||||
for which in ['start', 'end']
|
||||
(@$f "#{which}_date_date").attr 'disabled', b
|
||||
(@$f "#{which}_date_time").attr 'disabled', b
|
||||
|
||||
API.View.AssetRowView = class AssetRowView extends Backbone.View
|
||||
tagName: "tr"
|
||||
|
||||
initialize: (options) =>
|
||||
@template = get_template 'asset-row'
|
||||
|
||||
render: =>
|
||||
@$el.html @template _.extend json = @model.toJSON(),
|
||||
name: insertWbr truncate_str json.name # word break urls at slashes
|
||||
duration: durationSecondsToHumanReadable(json.duration)
|
||||
start_date: (date_to json.start_date).string()
|
||||
end_date: (date_to json.end_date).string()
|
||||
@$el.prop 'id', @model.get 'asset_id'
|
||||
(@$ ".delete-asset-button").popover content: get_template 'confirm-delete'
|
||||
(@$ ".toggle input").prop "checked", @model.get 'is_enabled'
|
||||
(@$ ".asset-icon").addClass switch @model.get "mimetype"
|
||||
when "video" then "fas fa-video"
|
||||
when "streaming" then "fas fa-video"
|
||||
when "image" then "far fa-image"
|
||||
when "webpage" then "fas fa-globe-americas"
|
||||
else ""
|
||||
|
||||
if (@model.get "is_processing") == 1
|
||||
(@$ 'input, button').prop 'disabled', on
|
||||
(@$ ".asset-toggle").html get_template 'processing-message'
|
||||
|
||||
@el
|
||||
|
||||
events:
|
||||
'change .is_enabled-toggle input': 'toggleIsEnabled'
|
||||
'click .download-asset-button': 'download'
|
||||
'click .edit-asset-button': 'edit'
|
||||
'click .delete-asset-button': 'showPopover'
|
||||
|
||||
toggleIsEnabled: (e) =>
|
||||
val = (1 + @model.get 'is_enabled') % 2
|
||||
@model.set is_enabled: val
|
||||
@setEnabled off
|
||||
save = @model.save()
|
||||
save.done => @setEnabled on
|
||||
save.fail =>
|
||||
@model.set @model.previousAttributes(), silent:yes # revert changes
|
||||
@setEnabled on
|
||||
@render()
|
||||
yes
|
||||
|
||||
setEnabled: (enabled) => if enabled
|
||||
@$el.removeClass 'warning'
|
||||
@delegateEvents()
|
||||
(@$ 'input, button').prop 'disabled', off
|
||||
else
|
||||
@hidePopover()
|
||||
@undelegateEvents()
|
||||
@$el.addClass 'warning'
|
||||
(@$ 'input, button').prop 'disabled', on
|
||||
|
||||
download: (e) =>
|
||||
$.get '/api/v2/assets/' + @model.id + '/content', (result) ->
|
||||
switch result['type']
|
||||
when 'url'
|
||||
window.open(result['url'])
|
||||
when 'file'
|
||||
content = base64js.toByteArray(result['content'])
|
||||
|
||||
mimetype = result['mimetype']
|
||||
fn = result['filename']
|
||||
|
||||
blob = new Blob([content], {type: mimetype})
|
||||
url = URL.createObjectURL(blob)
|
||||
|
||||
a = document.createElement('a')
|
||||
document.body.appendChild(a)
|
||||
a.download = fn
|
||||
a.href = url
|
||||
a.click()
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
a.remove()
|
||||
no
|
||||
|
||||
edit: (e) =>
|
||||
new EditAssetView model: @model
|
||||
no
|
||||
|
||||
delete: (e) =>
|
||||
@hidePopover()
|
||||
if (xhr = @model.destroy()) is not false
|
||||
xhr.done => @remove()
|
||||
else
|
||||
@remove()
|
||||
no
|
||||
|
||||
showPopover: =>
|
||||
if not ($ '.popover').length
|
||||
(@$ ".delete-asset-button").popover 'show'
|
||||
($ '.confirm-delete').click @delete
|
||||
($ window).one 'click', @hidePopover
|
||||
no
|
||||
|
||||
hidePopover: =>
|
||||
(@$ ".delete-asset-button").popover 'hide'
|
||||
no
|
||||
|
||||
|
||||
API.View.AssetsView = class AssetsView extends Backbone.View
|
||||
initialize: (options) =>
|
||||
@collection.bind event, @render for event in ('reset add remove sync'.split ' ')
|
||||
@sorted = (@$ '#active-assets').sortable
|
||||
containment: 'parent'
|
||||
axis: 'y'
|
||||
helper: 'clone'
|
||||
update: @update_order
|
||||
|
||||
update_order: =>
|
||||
active = (@$ '#active-assets').sortable 'toArray'
|
||||
|
||||
@collection.get(id).set('play_order', i) for id, i in active
|
||||
@collection.get(el.id).set('play_order', active.length) for el in (@$ '#inactive-assets tr').toArray()
|
||||
|
||||
$.post '/api/v2/assets/order', ids: ((@$ '#active-assets').sortable 'toArray').join ','
|
||||
|
||||
render: =>
|
||||
@collection.sort()
|
||||
|
||||
(@$ "##{which}-assets").html '' for which in ['active', 'inactive']
|
||||
|
||||
@collection.each (model) =>
|
||||
which = if model.active() then 'active' else 'inactive'
|
||||
(@$ "##{which}-assets").append (new AssetRowView model: model).render()
|
||||
|
||||
for which in ['active', 'inactive']
|
||||
if (@$ "##{which}-assets tr").length == 0
|
||||
(@$ "##{which}-assets-section .table-assets-help-text").show()
|
||||
else
|
||||
(@$ "##{which}-assets-section .table-assets-help-text").hide()
|
||||
|
||||
for which in ['inactive', 'active']
|
||||
@$(".#{which}-table thead").toggle !!(@$("##{which}-assets tr").length)
|
||||
|
||||
@update_order()
|
||||
|
||||
@el
|
||||
|
||||
|
||||
API.App = class App extends Backbone.View
|
||||
initialize: =>
|
||||
($ window).ajaxError (e,r) ->
|
||||
($ '#request-error').html (get_template 'request-error')()
|
||||
if (j = $.parseJSON r.responseText) and (err = j.error)
|
||||
($ '#request-error .msg').text 'Server Error: ' + err
|
||||
($ '#request-error').show()
|
||||
setTimeout ->
|
||||
($ '#request-error').fadeOut('slow')
|
||||
, 5000
|
||||
($ window).ajaxSuccess (event, request, settings) ->
|
||||
if (settings.url == new Assets().url) and (settings.type == 'POST')
|
||||
($ '#request-error').html (get_template 'request-success')()
|
||||
($ '#request-error .msg').text 'Asset has been successfully uploaded.'
|
||||
($ '#request-error').show()
|
||||
setTimeout ->
|
||||
($ '#request-error').fadeOut('slow')
|
||||
, 5000
|
||||
|
||||
(API.assets = new Assets()).fetch()
|
||||
API.assetsView = new AssetsView
|
||||
collection: API.assets
|
||||
el: @$ '#assets'
|
||||
|
||||
for address in wsAddresses
|
||||
try
|
||||
ws = new WebSocket address
|
||||
ws.onmessage = (x) ->
|
||||
x.data.text().then (assetId) ->
|
||||
model = API.assets.get(assetId)
|
||||
if model
|
||||
save = model.fetch()
|
||||
catch error
|
||||
no
|
||||
|
||||
events:
|
||||
'click .add-asset-button': 'add',
|
||||
'click #previous-asset-button': 'previous',
|
||||
'click #next-asset-button': 'next'
|
||||
|
||||
add: (e) ->
|
||||
new AddAssetView
|
||||
no
|
||||
|
||||
previous: (e) ->
|
||||
$.get '/api/v2/assets/control/previous'
|
||||
|
||||
next: (e) ->
|
||||
$.get '/api/v2/assets/control/next'
|
||||
42
static/js/backbone-0.9.10.min.js
vendored
42
static/js/backbone-0.9.10.min.js
vendored
@@ -1,42 +0,0 @@
|
||||
// Backbone.js 0.9.10
|
||||
|
||||
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Backbone may be freely distributed under the MIT license.
|
||||
// For all details and documentation:
|
||||
// http://backbonejs.org
|
||||
(function(){var n=this,B=n.Backbone,h=[],C=h.push,u=h.slice,D=h.splice,g;g="undefined"!==typeof exports?exports:n.Backbone={};g.VERSION="0.9.10";var f=n._;!f&&"undefined"!==typeof require&&(f=require("underscore"));g.$=n.jQuery||n.Zepto||n.ender;g.noConflict=function(){n.Backbone=B;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var v=/\s+/,q=function(a,b,c,d){if(!c)return!0;if("object"===typeof c)for(var e in c)a[b].apply(a,[e,c[e]].concat(d));else if(v.test(c)){c=c.split(v);e=0;for(var f=c.length;e<
|
||||
f;e++)a[b].apply(a,[c[e]].concat(d))}else return!0},w=function(a,b){var c,d=-1,e=a.length;switch(b.length){case 0:for(;++d<e;)(c=a[d]).callback.call(c.ctx);break;case 1:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0]);break;case 2:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0],b[1]);break;case 3:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0],b[1],b[2]);break;default:for(;++d<e;)(c=a[d]).callback.apply(c.ctx,b)}},h=g.Events={on:function(a,b,c){if(!q(this,"on",a,[b,c])||!b)return this;this._events||(this._events=
|
||||
{});(this._events[a]||(this._events[a]=[])).push({callback:b,context:c,ctx:c||this});return this},once:function(a,b,c){if(!q(this,"once",a,[b,c])||!b)return this;var d=this,e=f.once(function(){d.off(a,e);b.apply(this,arguments)});e._callback=b;this.on(a,e,c);return this},off:function(a,b,c){var d,e,t,g,j,l,k,h;if(!this._events||!q(this,"off",a,[b,c]))return this;if(!a&&!b&&!c)return this._events={},this;g=a?[a]:f.keys(this._events);j=0;for(l=g.length;j<l;j++)if(a=g[j],d=this._events[a]){t=[];if(b||
|
||||
c){k=0;for(h=d.length;k<h;k++)e=d[k],(b&&b!==e.callback&&b!==e.callback._callback||c&&c!==e.context)&&t.push(e)}this._events[a]=t}return this},trigger:function(a){if(!this._events)return this;var b=u.call(arguments,1);if(!q(this,"trigger",a,b))return this;var c=this._events[a],d=this._events.all;c&&w(c,b);d&&w(d,arguments);return this},listenTo:function(a,b,c){var d=this._listeners||(this._listeners={}),e=a._listenerId||(a._listenerId=f.uniqueId("l"));d[e]=a;a.on(b,"object"===typeof b?this:c,this);
|
||||
return this},stopListening:function(a,b,c){var d=this._listeners;if(d){if(a)a.off(b,"object"===typeof b?this:c,this),!b&&!c&&delete d[a._listenerId];else{"object"===typeof b&&(c=this);for(var e in d)d[e].off(b,c,this);this._listeners={}}return this}}};h.bind=h.on;h.unbind=h.off;f.extend(g,h);var r=g.Model=function(a,b){var c,d=a||{};this.cid=f.uniqueId("c");this.attributes={};b&&b.collection&&(this.collection=b.collection);b&&b.parse&&(d=this.parse(d,b)||{});if(c=f.result(this,"defaults"))d=f.defaults({},
|
||||
d,c);this.set(d,b);this.changed={};this.initialize.apply(this,arguments)};f.extend(r.prototype,h,{changed:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},sync:function(){return g.sync.apply(this,arguments)},get:function(a){return this.attributes[a]},escape:function(a){return f.escape(this.get(a))},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e,g,p,j,l,k;if(null==a)return this;"object"===typeof a?(e=a,c=b):(e={})[a]=b;c||(c={});
|
||||
if(!this._validate(e,c))return!1;g=c.unset;p=c.silent;a=[];j=this._changing;this._changing=!0;j||(this._previousAttributes=f.clone(this.attributes),this.changed={});k=this.attributes;l=this._previousAttributes;this.idAttribute in e&&(this.id=e[this.idAttribute]);for(d in e)b=e[d],f.isEqual(k[d],b)||a.push(d),f.isEqual(l[d],b)?delete this.changed[d]:this.changed[d]=b,g?delete k[d]:k[d]=b;if(!p){a.length&&(this._pending=!0);b=0;for(d=a.length;b<d;b++)this.trigger("change:"+a[b],this,k[a[b]],c)}if(j)return this;
|
||||
if(!p)for(;this._pending;)this._pending=!1,this.trigger("change",this,c);this._changing=this._pending=!1;return this},unset:function(a,b){return this.set(a,void 0,f.extend({},b,{unset:!0}))},clear:function(a){var b={},c;for(c in this.attributes)b[c]=void 0;return this.set(b,f.extend({},a,{unset:!0}))},hasChanged:function(a){return null==a?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._changing?
|
||||
this._previousAttributes:this.attributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return null==a||!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=a.success;a.success=function(a,d,e){if(!a.set(a.parse(d,e),e))return!1;b&&b(a,d,e)};return this.sync("read",this,a)},save:function(a,b,c){var d,e,g=this.attributes;
|
||||
null==a||"object"===typeof a?(d=a,c=b):(d={})[a]=b;if(d&&(!c||!c.wait)&&!this.set(d,c))return!1;c=f.extend({validate:!0},c);if(!this._validate(d,c))return!1;d&&c.wait&&(this.attributes=f.extend({},g,d));void 0===c.parse&&(c.parse=!0);e=c.success;c.success=function(a,b,c){a.attributes=g;var k=a.parse(b,c);c.wait&&(k=f.extend(d||{},k));if(f.isObject(k)&&!a.set(k,c))return!1;e&&e(a,b,c)};a=this.isNew()?"create":c.patch?"patch":"update";"patch"===a&&(c.attrs=d);a=this.sync(a,this,c);d&&c.wait&&(this.attributes=
|
||||
g);return a},destroy:function(a){a=a?f.clone(a):{};var b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};a.success=function(a,b,e){(e.wait||a.isNew())&&d();c&&c(a,b,e)};if(this.isNew())return a.success(this,null,a),!1;var e=this.sync("delete",this,a);a.wait||d();return e},url:function(){var a=f.result(this,"urlRoot")||f.result(this.collection,"url")||x();return this.isNew()?a:a+("/"===a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},
|
||||
isNew:function(){return null==this.id},isValid:function(a){return!this.validate||!this.validate(this.attributes,a)},_validate:function(a,b){if(!b.validate||!this.validate)return!0;a=f.extend({},this.attributes,a);var c=this.validationError=this.validate(a,b)||null;if(!c)return!0;this.trigger("invalid",this,c,b||{});return!1}});var s=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);void 0!==b.comparator&&(this.comparator=b.comparator);this.models=[];this._reset();this.initialize.apply(this,
|
||||
arguments);a&&this.reset(a,f.extend({silent:!0},b))};f.extend(s.prototype,h,{model:r,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},sync:function(){return g.sync.apply(this,arguments)},add:function(a,b){a=f.isArray(a)?a.slice():[a];b||(b={});var c,d,e,g,p,j,l,k,h,m;l=[];k=b.at;h=this.comparator&&null==k&&!1!=b.sort;m=f.isString(this.comparator)?this.comparator:null;c=0;for(d=a.length;c<d;c++)(e=this._prepareModel(g=a[c],b))?(p=this.get(e))?b.merge&&(p.set(g===
|
||||
e?e.attributes:g,b),h&&(!j&&p.hasChanged(m))&&(j=!0)):(l.push(e),e.on("all",this._onModelEvent,this),this._byId[e.cid]=e,null!=e.id&&(this._byId[e.id]=e)):this.trigger("invalid",this,g,b);l.length&&(h&&(j=!0),this.length+=l.length,null!=k?D.apply(this.models,[k,0].concat(l)):C.apply(this.models,l));j&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=l.length;c<d;c++)(e=l[c]).trigger("add",e,this,b);j&&this.trigger("sort",this,b);return this},remove:function(a,b){a=f.isArray(a)?a.slice():[a];
|
||||
b||(b={});var c,d,e,g;c=0;for(d=a.length;c<d;c++)if(g=this.get(a[c]))delete this._byId[g.id],delete this._byId[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:this.length},b));return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},
|
||||
b));return a},shift:function(a){var b=this.at(0);this.remove(b,a);return b},slice:function(a,b){return this.models.slice(a,b)},get:function(a){if(null!=a)return this._idAttr||(this._idAttr=this.model.prototype.idAttribute),this._byId[a.id||a.cid||a[this._idAttr]||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){if(!this.comparator)throw Error("Cannot sort a set without a comparator");
|
||||
a||(a={});f.isString(this.comparator)||1===this.comparator.length?this.models=this.sortBy(this.comparator,this):this.models.sort(f.bind(this.comparator,this));a.silent||this.trigger("sort",this,a);return this},pluck:function(a){return f.invoke(this.models,"get",a)},update:function(a,b){b=f.extend({add:!0,merge:!0,remove:!0},b);b.parse&&(a=this.parse(a,b));var c,d,e,g,h=[],j=[],l={};f.isArray(a)||(a=a?[a]:[]);if(b.add&&!b.remove)return this.add(a,b);d=0;for(e=a.length;d<e;d++)c=a[d],g=this.get(c),
|
||||
b.remove&&g&&(l[g.cid]=!0),(b.add&&!g||b.merge&&g)&&h.push(c);if(b.remove){d=0;for(e=this.models.length;d<e;d++)c=this.models[d],l[c.cid]||j.push(c)}j.length&&this.remove(j,b);h.length&&this.add(h,b);return this},reset:function(a,b){b||(b={});b.parse&&(a=this.parse(a,b));for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);b.previousModels=this.models.slice();this._reset();a&&this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=
|
||||
a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=a.success;a.success=function(a,d,e){a[e.update?"update":"reset"](d,e);b&&b(a,d,e)};return this.sync("read",this,a)},create:function(a,b){b=b?f.clone(b):{};if(!(a=this._prepareModel(a,b)))return!1;b.wait||this.add(a,b);var c=this,d=b.success;b.success=function(a,b,f){f.wait&&c.add(a,f);d&&d(a,b,f)};a.save(null,b);return a},parse:function(a){return a},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models.length=
|
||||
0;this._byId={}},_prepareModel:function(a,b){if(a instanceof r)return a.collection||(a.collection=this),a;b||(b={});b.collection=this;var c=new this.model(a,b);return!c._validate(a,b)?!1:c},_removeReference:function(a){this===a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"===a||"remove"===a)&&c!==this||("destroy"===a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],null!=b.id&&(this._byId[b.id]=
|
||||
b)),this.trigger.apply(this,arguments))},sortedIndex:function(a,b,c){b||(b=this.comparator);var d=f.isFunction(b)?b:function(a){return a.get(b)};return f.sortedIndex(this.models,a,d,c)}});f.each("forEach each map collect reduce foldl inject reduceRight foldr find detect filter select reject every all some any include contains invoke max min toArray size first head take initial rest tail drop last without indexOf shuffle lastIndexOf isEmpty chain".split(" "),function(a){s.prototype[a]=function(){var b=
|
||||
u.call(arguments);b.unshift(this.models);return f[a].apply(f,b)}});f.each(["groupBy","countBy","sortBy"],function(a){s.prototype[a]=function(b,c){var d=f.isFunction(b)?b:function(a){return a.get(b)};return f[a](this.models,d,c)}});var y=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},E=/\((.*?)\)/g,F=/(\(\?)?:\w+/g,G=/\*\w+/g,H=/[\-{}\[\]+?.,\\\^$|#\s]/g;f.extend(y.prototype,h,{initialize:function(){},route:function(a,b,c){f.isRegExp(a)||
|
||||
(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));this.trigger("route",b,d);g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b);return this},_bindRoutes:function(){if(this.routes)for(var a,b=f.keys(this.routes);null!=(a=b.pop());)this.route(a,this.routes[a])},_routeToRegExp:function(a){a=a.replace(H,"\\$&").replace(E,"(?:$1)?").replace(F,
|
||||
function(a,c){return c?a:"([^/]+)"}).replace(G,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl");"undefined"!==typeof window&&(this.location=window.location,this.history=window.history)},z=/^[#\/]|\s+$/g,I=/^\/+|\/+$/g,J=/msie [\w.]+/,K=/\/$/;m.started=!1;f.extend(m.prototype,h,{interval:50,getHash:function(a){return(a=(a||this).location.href.match(/#(.*)$/))?a[1]:""},getFragment:function(a,
|
||||
b){if(null==a)if(this._hasPushState||!this._wantsHashChange||b){a=this.location.pathname;var c=this.root.replace(K,"");a.indexOf(c)||(a=a.substr(c.length))}else a=this.getHash();return a.replace(z,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this.root=this.options.root;this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||
|
||||
!this.history||!this.history.pushState);a=this.getFragment();var b=document.documentMode,b=J.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b);this.root=("/"+this.root+"/").replace(I,"/");b&&this._wantsHashChange&&(this.iframe=g.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a));if(this._hasPushState)g.$(window).on("popstate",this.checkUrl);else if(this._wantsHashChange&&"onhashchange"in window&&!b)g.$(window).on("hashchange",this.checkUrl);
|
||||
else this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,this.interval));this.fragment=a;a=this.location;b=a.pathname.replace(/[^\/]$/,"$&/")===this.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),this.location.replace(this.root+this.location.search+"#"+this.fragment),!0;this._wantsPushState&&(this._hasPushState&&b&&a.hash)&&(this.fragment=this.getHash().replace(z,""),this.history.replaceState({},document.title,
|
||||
this.root+this.fragment+a.search));if(!this.options.silent)return this.loadUrl()},stop:function(){g.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a===this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a===this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},
|
||||
loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};a=this.getFragment(a||"");if(this.fragment!==a){this.fragment=a;var c=this.root+a;if(this._hasPushState)this.history[b.replace?"replaceState":"pushState"]({},document.title,c);else if(this._wantsHashChange)this._updateHash(this.location,a,b.replace),this.iframe&&a!==this.getFragment(this.getHash(this.iframe))&&
|
||||
(b.replace||this.iframe.document.open().close(),this._updateHash(this.iframe.location,a,b.replace));else return this.location.assign(c);b.trigger&&this.loadUrl(a)}},_updateHash:function(a,b,c){c?(c=a.href.replace(/(javascript:|#).*$/,""),a.replace(c+"#"+b)):a.hash="#"+b}});g.history=new m;var A=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},L=/^(\S+)\s*(.*)$/,M="model collection el id attributes className tagName events".split(" ");
|
||||
f.extend(A.prototype,h,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof g.$?a:g.$(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=f.result(this,"events"))){this.undelegateEvents();for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);
|
||||
if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(L),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);if(""===d)this.$el.on(e,c);else this.$el.on(e,d,c)}}},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},f.result(this,"options"),a));f.extend(this,f.pick(a,M));this.options=a},_ensureElement:function(){if(this.el)this.setElement(f.result(this,"el"),!1);else{var a=f.extend({},f.result(this,"attributes"));
|
||||
this.id&&(a.id=f.result(this,"id"));this.className&&(a["class"]=f.result(this,"className"));a=g.$("<"+f.result(this,"tagName")+">").attr(a);this.setElement(a,!1)}}});var N={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=N[a];f.defaults(c||(c={}),{emulateHTTP:g.emulateHTTP,emulateJSON:g.emulateJSON});var e={type:d,dataType:"json"};c.url||(e.url=f.result(b,"url")||x());if(null==c.data&&b&&("create"===a||"update"===a||"patch"===a))e.contentType="application/json",
|
||||
e.data=JSON.stringify(c.attrs||b.toJSON(c));c.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(c.emulateHTTP&&("PUT"===d||"DELETE"===d||"PATCH"===d)){e.type="POST";c.emulateJSON&&(e.data._method=d);var h=c.beforeSend;c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d);if(h)return h.apply(this,arguments)}}"GET"!==e.type&&!c.emulateJSON&&(e.processData=!1);var m=c.success;c.success=function(a){m&&m(b,a,c);b.trigger("sync",b,a,c)};
|
||||
var j=c.error;c.error=function(a){j&&j(b,a,c);b.trigger("error",b,a,c)};a=c.xhr=g.ajax(f.extend(e,c));b.trigger("request",b,a,c);return a};g.ajax=function(){return g.$.ajax.apply(g.$,arguments)};r.extend=s.extend=y.extend=A.extend=m.extend=function(a,b){var c=this,d;d=a&&f.has(a,"constructor")?a.constructor:function(){return c.apply(this,arguments)};f.extend(d,c,b);var e=function(){this.constructor=d};e.prototype=c.prototype;d.prototype=new e;a&&f.extend(d.prototype,a);d.__super__=c.prototype;return d};
|
||||
var x=function(){throw Error('A "url" property or function must be specified');}}).call(this);
|
||||
1
static/js/base64js.min.js
vendored
1
static/js/base64js.min.js
vendored
@@ -1 +0,0 @@
|
||||
(function(r){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=r()}else if(typeof define==="function"&&define.amd){define([],r)}else{var e;if(typeof window!=="undefined"){e=window}else if(typeof global!=="undefined"){e=global}else if(typeof self!=="undefined"){e=self}else{e=this}e.base64js=r()}})(function(){var r,e,n;return function(){function r(e,n,t){function o(f,i){if(!n[f]){if(!e[f]){var u="function"==typeof require&&require;if(!i&&u)return u(f,!0);if(a)return a(f,!0);var v=new Error("Cannot find module '"+f+"'");throw v.code="MODULE_NOT_FOUND",v}var d=n[f]={exports:{}};e[f][0].call(d.exports,function(r){var n=e[f][1][r];return o(n||r)},d,d.exports,r,e,n,t)}return n[f].exports}for(var a="function"==typeof require&&require,f=0;f<t.length;f++)o(t[f]);return o}return r}()({"/":[function(r,e,n){"use strict";n.byteLength=d;n.toByteArray=h;n.fromByteArray=p;var t=[];var o=[];var a=typeof Uint8Array!=="undefined"?Uint8Array:Array;var f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";for(var i=0,u=f.length;i<u;++i){t[i]=f[i];o[f.charCodeAt(i)]=i}o["-".charCodeAt(0)]=62;o["_".charCodeAt(0)]=63;function v(r){var e=r.length;if(e%4>0){throw new Error("Invalid string. Length must be a multiple of 4")}var n=r.indexOf("=");if(n===-1)n=e;var t=n===e?0:4-n%4;return[n,t]}function d(r){var e=v(r);var n=e[0];var t=e[1];return(n+t)*3/4-t}function c(r,e,n){return(e+n)*3/4-n}function h(r){var e;var n=v(r);var t=n[0];var f=n[1];var i=new a(c(r,t,f));var u=0;var d=f>0?t-4:t;for(var h=0;h<d;h+=4){e=o[r.charCodeAt(h)]<<18|o[r.charCodeAt(h+1)]<<12|o[r.charCodeAt(h+2)]<<6|o[r.charCodeAt(h+3)];i[u++]=e>>16&255;i[u++]=e>>8&255;i[u++]=e&255}if(f===2){e=o[r.charCodeAt(h)]<<2|o[r.charCodeAt(h+1)]>>4;i[u++]=e&255}if(f===1){e=o[r.charCodeAt(h)]<<10|o[r.charCodeAt(h+1)]<<4|o[r.charCodeAt(h+2)]>>2;i[u++]=e>>8&255;i[u++]=e&255}return i}function s(r){return t[r>>18&63]+t[r>>12&63]+t[r>>6&63]+t[r&63]}function l(r,e,n){var t;var o=[];for(var a=e;a<n;a+=3){t=(r[a]<<16&16711680)+(r[a+1]<<8&65280)+(r[a+2]&255);o.push(s(t))}return o.join("")}function p(r){var e;var n=r.length;var o=n%3;var a=[];var f=16383;for(var i=0,u=n-o;i<u;i+=f){a.push(l(r,i,i+f>u?u:i+f))}if(o===1){e=r[n-1];a.push(t[e>>2]+t[e<<4&63]+"==")}else if(o===2){e=(r[n-2]<<8)+r[n-1];a.push(t[e>>10]+t[e>>4&63]+t[e<<2&63]+"=")}return a.join("")}},{}]},{},[])("/")});
|
||||
960
static/js/bootstrap-datepicker.js
vendored
960
static/js/bootstrap-datepicker.js
vendored
@@ -1,960 +0,0 @@
|
||||
/* =========================================================
|
||||
* bootstrap-datepicker.js
|
||||
* http://www.eyecon.ro/bootstrap-datepicker
|
||||
* =========================================================
|
||||
* Copyright 2012 Stefan Petre
|
||||
* Improvements by Andrew Rowls
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* ========================================================= */
|
||||
|
||||
!function( $ ) {
|
||||
|
||||
function UTCDate(){
|
||||
return new Date(Date.UTC.apply(Date, arguments));
|
||||
}
|
||||
function UTCToday(){
|
||||
var today = new Date();
|
||||
return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
|
||||
}
|
||||
|
||||
// Picker object
|
||||
|
||||
var Datepicker = function(element, options) {
|
||||
var that = this;
|
||||
|
||||
this.element = $(element);
|
||||
this.language = options.language||this.element.data('date-language')||"en";
|
||||
this.language = this.language in dates ? this.language : "en";
|
||||
this.isRTL = dates[this.language].rtl||false;
|
||||
this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
|
||||
this.isInline = false;
|
||||
this.isInput = this.element.is('input');
|
||||
this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
|
||||
this.hasInput = this.component && this.element.find('input').length;
|
||||
if(this.component && this.component.length === 0)
|
||||
this.component = false;
|
||||
|
||||
this._attachEvents();
|
||||
|
||||
this.forceParse = true;
|
||||
if ('forceParse' in options) {
|
||||
this.forceParse = options.forceParse;
|
||||
} else if ('dateForceParse' in this.element.data()) {
|
||||
this.forceParse = this.element.data('date-force-parse');
|
||||
}
|
||||
|
||||
|
||||
this.picker = $(DPGlobal.template)
|
||||
.appendTo(this.isInline ? this.element : 'body')
|
||||
.on({
|
||||
click: $.proxy(this.click, this),
|
||||
mousedown: $.proxy(this.mousedown, this)
|
||||
});
|
||||
|
||||
if(this.isInline) {
|
||||
this.picker.addClass('datepicker-inline');
|
||||
} else {
|
||||
this.picker.addClass('datepicker-dropdown dropdown-menu');
|
||||
}
|
||||
if (this.isRTL){
|
||||
this.picker.addClass('datepicker-rtl');
|
||||
this.picker.find('.prev i, .next i')
|
||||
.toggleClass('fa-arrow-left fa-arrow-right');
|
||||
}
|
||||
$(document).on('mousedown', function (e) {
|
||||
// Clicked outside the datepicker, hide it
|
||||
if ($(e.target).closest('.datepicker').length === 0) {
|
||||
that.hide();
|
||||
}
|
||||
});
|
||||
|
||||
this.autoclose = false;
|
||||
if ('autoclose' in options) {
|
||||
this.autoclose = options.autoclose;
|
||||
} else if ('dateAutoclose' in this.element.data()) {
|
||||
this.autoclose = this.element.data('date-autoclose');
|
||||
}
|
||||
|
||||
this.keyboardNavigation = true;
|
||||
if ('keyboardNavigation' in options) {
|
||||
this.keyboardNavigation = options.keyboardNavigation;
|
||||
} else if ('dateKeyboardNavigation' in this.element.data()) {
|
||||
this.keyboardNavigation = this.element.data('date-keyboard-navigation');
|
||||
}
|
||||
|
||||
this.viewMode = this.startViewMode = 0;
|
||||
switch(options.startView || this.element.data('date-start-view')){
|
||||
case 2:
|
||||
case 'decade':
|
||||
this.viewMode = this.startViewMode = 2;
|
||||
break;
|
||||
case 1:
|
||||
case 'year':
|
||||
this.viewMode = this.startViewMode = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false);
|
||||
this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false);
|
||||
|
||||
this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7);
|
||||
this.weekEnd = ((this.weekStart + 6) % 7);
|
||||
this.startDate = -Infinity;
|
||||
this.endDate = Infinity;
|
||||
this.daysOfWeekDisabled = [];
|
||||
this.setStartDate(options.startDate||this.element.data('date-startdate'));
|
||||
this.setEndDate(options.endDate||this.element.data('date-enddate'));
|
||||
this.setDaysOfWeekDisabled(options.daysOfWeekDisabled||this.element.data('date-days-of-week-disabled'));
|
||||
this.fillDow();
|
||||
this.fillMonths();
|
||||
this.update();
|
||||
this.showMode();
|
||||
|
||||
if(this.isInline) {
|
||||
this.show();
|
||||
}
|
||||
};
|
||||
|
||||
Datepicker.prototype = {
|
||||
constructor: Datepicker,
|
||||
|
||||
_events: [],
|
||||
_attachEvents: function(){
|
||||
this._detachEvents();
|
||||
if (this.isInput) { // single input
|
||||
this._events = [
|
||||
[this.element, {
|
||||
focus: $.proxy(this.show, this),
|
||||
keyup: $.proxy(this.update, this),
|
||||
keydown: $.proxy(this.keydown, this)
|
||||
}]
|
||||
];
|
||||
}
|
||||
else if (this.component && this.hasInput){ // component: input + button
|
||||
this._events = [
|
||||
// For components that are not readonly, allow keyboard nav
|
||||
[this.element.find('input'), {
|
||||
focus: $.proxy(this.show, this),
|
||||
keyup: $.proxy(this.update, this),
|
||||
keydown: $.proxy(this.keydown, this)
|
||||
}],
|
||||
[this.component, {
|
||||
click: $.proxy(this.show, this)
|
||||
}]
|
||||
];
|
||||
}
|
||||
else if (this.element.is('div')) { // inline datepicker
|
||||
this.isInline = true;
|
||||
}
|
||||
else {
|
||||
this._events = [
|
||||
[this.element, {
|
||||
click: $.proxy(this.show, this)
|
||||
}]
|
||||
];
|
||||
}
|
||||
for (var i=0, el, ev; i<this._events.length; i++){
|
||||
el = this._events[i][0];
|
||||
ev = this._events[i][1];
|
||||
el.on(ev);
|
||||
}
|
||||
},
|
||||
_detachEvents: function(){
|
||||
for (var i=0, el, ev; i<this._events.length; i++){
|
||||
el = this._events[i][0];
|
||||
ev = this._events[i][1];
|
||||
el.off(ev);
|
||||
}
|
||||
this._events = [];
|
||||
},
|
||||
|
||||
show: function(e) {
|
||||
this.picker.show();
|
||||
this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
|
||||
this.update();
|
||||
this.place();
|
||||
$(window).on('resize', $.proxy(this.place, this));
|
||||
if (e ) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.element.trigger({
|
||||
type: 'show',
|
||||
date: this.date
|
||||
});
|
||||
},
|
||||
|
||||
hide: function(e){
|
||||
if(this.isInline) return;
|
||||
this.picker.hide();
|
||||
$(window).off('resize', this.place);
|
||||
this.viewMode = this.startViewMode;
|
||||
this.showMode();
|
||||
if (!this.isInput) {
|
||||
$(document).off('mousedown', this.hide);
|
||||
}
|
||||
|
||||
if (
|
||||
this.forceParse &&
|
||||
(
|
||||
this.isInput && this.element.val() ||
|
||||
this.hasInput && this.element.find('input').val()
|
||||
)
|
||||
)
|
||||
this.setValue();
|
||||
this.element.trigger({
|
||||
type: 'hide',
|
||||
date: this.date
|
||||
});
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
this._detachEvents();
|
||||
this.picker.remove();
|
||||
delete this.element.data().datepicker;
|
||||
},
|
||||
|
||||
getDate: function() {
|
||||
var d = this.getUTCDate();
|
||||
return new Date(d.getTime() + (d.getTimezoneOffset()*60000));
|
||||
},
|
||||
|
||||
getUTCDate: function() {
|
||||
return this.date;
|
||||
},
|
||||
|
||||
setDate: function(d) {
|
||||
this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000)));
|
||||
},
|
||||
|
||||
setUTCDate: function(d) {
|
||||
this.date = d;
|
||||
this.setValue();
|
||||
},
|
||||
|
||||
setValue: function() {
|
||||
var formatted = this.getFormattedDate();
|
||||
if (!this.isInput) {
|
||||
if (this.component){
|
||||
this.element.find('input').prop('value', formatted);
|
||||
}
|
||||
this.element.data('date', formatted);
|
||||
} else {
|
||||
this.element.prop('value', formatted);
|
||||
}
|
||||
},
|
||||
|
||||
getFormattedDate: function(format) {
|
||||
if(format == undefined) format = this.format;
|
||||
return DPGlobal.formatDate(this.date, format, this.language);
|
||||
},
|
||||
|
||||
setStartDate: function(startDate){
|
||||
this.startDate = startDate||-Infinity;
|
||||
if (this.startDate !== -Infinity) {
|
||||
this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language);
|
||||
}
|
||||
this.update();
|
||||
this.updateNavArrows();
|
||||
},
|
||||
|
||||
setEndDate: function(endDate){
|
||||
this.endDate = endDate||Infinity;
|
||||
if (this.endDate !== Infinity) {
|
||||
this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language);
|
||||
}
|
||||
this.update();
|
||||
this.updateNavArrows();
|
||||
},
|
||||
|
||||
setDaysOfWeekDisabled: function(daysOfWeekDisabled){
|
||||
this.daysOfWeekDisabled = daysOfWeekDisabled||[];
|
||||
if (!$.isArray(this.daysOfWeekDisabled)) {
|
||||
this.daysOfWeekDisabled = this.daysOfWeekDisabled.split(/,\s*/);
|
||||
}
|
||||
this.daysOfWeekDisabled = $.map(this.daysOfWeekDisabled, function (d) {
|
||||
return parseInt(d, 10);
|
||||
});
|
||||
this.update();
|
||||
this.updateNavArrows();
|
||||
},
|
||||
|
||||
place: function(){
|
||||
if(this.isInline) return;
|
||||
var zIndex = parseInt(this.element.parents().filter(function() {
|
||||
return $(this).css('z-index') != 'auto';
|
||||
}).first().css('z-index'))+10;
|
||||
var offset = this.component ? this.component.offset() : this.element.offset();
|
||||
this.picker.css({
|
||||
top: offset.top + this.height,
|
||||
left: offset.left,
|
||||
zIndex: zIndex
|
||||
});
|
||||
},
|
||||
|
||||
update: function(){
|
||||
var date, fromArgs = false;
|
||||
if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
|
||||
date = arguments[0];
|
||||
fromArgs = true;
|
||||
} else {
|
||||
date = this.isInput ? this.element.prop('value') : this.element.data('date') || this.element.find('input').prop('value');
|
||||
}
|
||||
|
||||
this.date = DPGlobal.parseDate(date, this.format, this.language);
|
||||
|
||||
if(fromArgs) this.setValue();
|
||||
|
||||
var oldViewDate = this.viewDate;
|
||||
if (this.date < this.startDate) {
|
||||
this.viewDate = new Date(this.startDate);
|
||||
} else if (this.date > this.endDate) {
|
||||
this.viewDate = new Date(this.endDate);
|
||||
} else {
|
||||
this.viewDate = new Date(this.date);
|
||||
}
|
||||
|
||||
if (oldViewDate && oldViewDate.getTime() != this.viewDate.getTime()){
|
||||
this.element.trigger({
|
||||
type: 'changeDate',
|
||||
date: this.viewDate
|
||||
});
|
||||
}
|
||||
this.fill();
|
||||
},
|
||||
|
||||
fillDow: function(){
|
||||
var dowCnt = this.weekStart,
|
||||
html = '<tr>';
|
||||
while (dowCnt < this.weekStart + 7) {
|
||||
html += '<th class="dow">'+dates[this.language].daysMin[(dowCnt++)%7]+'</th>';
|
||||
}
|
||||
html += '</tr>';
|
||||
this.picker.find('.datepicker-days thead').append(html);
|
||||
},
|
||||
|
||||
fillMonths: function(){
|
||||
var html = '',
|
||||
i = 0;
|
||||
while (i < 12) {
|
||||
html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>';
|
||||
}
|
||||
this.picker.find('.datepicker-months td').html(html);
|
||||
},
|
||||
|
||||
fill: function() {
|
||||
var d = new Date(this.viewDate),
|
||||
year = d.getUTCFullYear(),
|
||||
month = d.getUTCMonth(),
|
||||
startYear = this.startDate !== -Infinity ? this.startDate.getUTCFullYear() : -Infinity,
|
||||
startMonth = this.startDate !== -Infinity ? this.startDate.getUTCMonth() : -Infinity,
|
||||
endYear = this.endDate !== Infinity ? this.endDate.getUTCFullYear() : Infinity,
|
||||
endMonth = this.endDate !== Infinity ? this.endDate.getUTCMonth() : Infinity,
|
||||
currentDate = this.date && this.date.valueOf(),
|
||||
today = new Date();
|
||||
this.picker.find('.datepicker-days thead th:eq(1)')
|
||||
.text(dates[this.language].months[month]+' '+year);
|
||||
this.picker.find('tfoot th.today')
|
||||
.text(dates[this.language].today)
|
||||
.toggle(this.todayBtn !== false);
|
||||
this.updateNavArrows();
|
||||
this.fillMonths();
|
||||
var prevMonth = UTCDate(year, month-1, 28,0,0,0,0),
|
||||
day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
|
||||
prevMonth.setUTCDate(day);
|
||||
prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7)%7);
|
||||
var nextMonth = new Date(prevMonth);
|
||||
nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
|
||||
nextMonth = nextMonth.valueOf();
|
||||
var html = [];
|
||||
var clsName;
|
||||
while(prevMonth.valueOf() < nextMonth) {
|
||||
if (prevMonth.getUTCDay() == this.weekStart) {
|
||||
html.push('<tr>');
|
||||
}
|
||||
clsName = '';
|
||||
if (prevMonth.getUTCFullYear() < year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() < month)) {
|
||||
clsName += ' old';
|
||||
} else if (prevMonth.getUTCFullYear() > year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() > month)) {
|
||||
clsName += ' new';
|
||||
}
|
||||
// Compare internal UTC date with local today, not UTC today
|
||||
if (this.todayHighlight &&
|
||||
prevMonth.getUTCFullYear() == today.getFullYear() &&
|
||||
prevMonth.getUTCMonth() == today.getMonth() &&
|
||||
prevMonth.getUTCDate() == today.getDate()) {
|
||||
clsName += ' today';
|
||||
}
|
||||
if (currentDate && prevMonth.valueOf() == currentDate) {
|
||||
clsName += ' active';
|
||||
}
|
||||
if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate ||
|
||||
$.inArray(prevMonth.getUTCDay(), this.daysOfWeekDisabled) !== -1) {
|
||||
clsName += ' disabled';
|
||||
}
|
||||
html.push('<td class="day'+clsName+'">'+prevMonth.getUTCDate() + '</td>');
|
||||
if (prevMonth.getUTCDay() == this.weekEnd) {
|
||||
html.push('</tr>');
|
||||
}
|
||||
prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
|
||||
}
|
||||
this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
|
||||
var currentYear = this.date && this.date.getUTCFullYear();
|
||||
|
||||
var months = this.picker.find('.datepicker-months')
|
||||
.find('th:eq(1)')
|
||||
.text(year)
|
||||
.end()
|
||||
.find('span').removeClass('active');
|
||||
if (currentYear && currentYear == year) {
|
||||
months.eq(this.date.getUTCMonth()).addClass('active');
|
||||
}
|
||||
if (year < startYear || year > endYear) {
|
||||
months.addClass('disabled');
|
||||
}
|
||||
if (year == startYear) {
|
||||
months.slice(0, startMonth).addClass('disabled');
|
||||
}
|
||||
if (year == endYear) {
|
||||
months.slice(endMonth+1).addClass('disabled');
|
||||
}
|
||||
|
||||
html = '';
|
||||
year = parseInt(year/10, 10) * 10;
|
||||
var yearCont = this.picker.find('.datepicker-years')
|
||||
.find('th:eq(1)')
|
||||
.text(year + '-' + (year + 9))
|
||||
.end()
|
||||
.find('td');
|
||||
year -= 1;
|
||||
for (var i = -1; i < 11; i++) {
|
||||
html += '<span class="year'+(i == -1 || i == 10 ? ' old' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>';
|
||||
year += 1;
|
||||
}
|
||||
yearCont.html(html);
|
||||
},
|
||||
|
||||
updateNavArrows: function() {
|
||||
var d = new Date(this.viewDate),
|
||||
year = d.getUTCFullYear(),
|
||||
month = d.getUTCMonth();
|
||||
switch (this.viewMode) {
|
||||
case 0:
|
||||
if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) {
|
||||
this.picker.find('.prev').css({visibility: 'hidden'});
|
||||
} else {
|
||||
this.picker.find('.prev').css({visibility: 'visible'});
|
||||
}
|
||||
if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() && month >= this.endDate.getUTCMonth()) {
|
||||
this.picker.find('.next').css({visibility: 'hidden'});
|
||||
} else {
|
||||
this.picker.find('.next').css({visibility: 'visible'});
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) {
|
||||
this.picker.find('.prev').css({visibility: 'hidden'});
|
||||
} else {
|
||||
this.picker.find('.prev').css({visibility: 'visible'});
|
||||
}
|
||||
if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear()) {
|
||||
this.picker.find('.next').css({visibility: 'hidden'});
|
||||
} else {
|
||||
this.picker.find('.next').css({visibility: 'visible'});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
click: function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
var target = $(e.target).closest('span, td, th');
|
||||
if (target.length == 1) {
|
||||
switch(target[0].nodeName.toLowerCase()) {
|
||||
case 'th':
|
||||
switch(target[0].className) {
|
||||
case 'switch':
|
||||
this.showMode(1);
|
||||
break;
|
||||
case 'prev':
|
||||
case 'next':
|
||||
var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1);
|
||||
switch(this.viewMode){
|
||||
case 0:
|
||||
this.viewDate = this.moveMonth(this.viewDate, dir);
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
this.viewDate = this.moveYear(this.viewDate, dir);
|
||||
break;
|
||||
}
|
||||
this.fill();
|
||||
break;
|
||||
case 'today':
|
||||
var date = new Date();
|
||||
date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
|
||||
|
||||
this.showMode(-2);
|
||||
var which = this.todayBtn == 'linked' ? null : 'view';
|
||||
this._setDate(date, which);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'span':
|
||||
if (!target.is('.disabled')) {
|
||||
this.viewDate.setUTCDate(1);
|
||||
if (target.is('.month')) {
|
||||
var month = target.parent().find('span').index(target);
|
||||
this.viewDate.setUTCMonth(month);
|
||||
this.element.trigger({
|
||||
type: 'changeMonth',
|
||||
date: this.viewDate
|
||||
});
|
||||
} else {
|
||||
var year = parseInt(target.text(), 10)||0;
|
||||
this.viewDate.setUTCFullYear(year);
|
||||
this.element.trigger({
|
||||
type: 'changeYear',
|
||||
date: this.viewDate
|
||||
});
|
||||
}
|
||||
this.showMode(-1);
|
||||
this.fill();
|
||||
}
|
||||
break;
|
||||
case 'td':
|
||||
if (target.is('.day') && !target.is('.disabled')){
|
||||
var day = parseInt(target.text(), 10)||1;
|
||||
var year = this.viewDate.getUTCFullYear(),
|
||||
month = this.viewDate.getUTCMonth();
|
||||
if (target.is('.old')) {
|
||||
if (month === 0) {
|
||||
month = 11;
|
||||
year -= 1;
|
||||
} else {
|
||||
month -= 1;
|
||||
}
|
||||
} else if (target.is('.new')) {
|
||||
if (month == 11) {
|
||||
month = 0;
|
||||
year += 1;
|
||||
} else {
|
||||
month += 1;
|
||||
}
|
||||
}
|
||||
this._setDate(UTCDate(year, month, day,0,0,0,0));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_setDate: function(date, which){
|
||||
if (!which || which == 'date')
|
||||
this.date = date;
|
||||
if (!which || which == 'view')
|
||||
this.viewDate = date;
|
||||
this.fill();
|
||||
this.setValue();
|
||||
this.element.trigger({
|
||||
type: 'changeDate',
|
||||
date: this.date
|
||||
});
|
||||
var element;
|
||||
if (this.isInput) {
|
||||
element = this.element;
|
||||
} else if (this.component){
|
||||
element = this.element.find('input');
|
||||
}
|
||||
if (element) {
|
||||
element.change();
|
||||
if (this.autoclose && (!which || which == 'date')) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
moveMonth: function(date, dir){
|
||||
if (!dir) return date;
|
||||
var new_date = new Date(date.valueOf()),
|
||||
day = new_date.getUTCDate(),
|
||||
month = new_date.getUTCMonth(),
|
||||
mag = Math.abs(dir),
|
||||
new_month, test;
|
||||
dir = dir > 0 ? 1 : -1;
|
||||
if (mag == 1){
|
||||
test = dir == -1
|
||||
// If going back one month, make sure month is not current month
|
||||
// (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
|
||||
? function(){ return new_date.getUTCMonth() == month; }
|
||||
// If going forward one month, make sure month is as expected
|
||||
// (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
|
||||
: function(){ return new_date.getUTCMonth() != new_month; };
|
||||
new_month = month + dir;
|
||||
new_date.setUTCMonth(new_month);
|
||||
// Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
|
||||
if (new_month < 0 || new_month > 11)
|
||||
new_month = (new_month + 12) % 12;
|
||||
} else {
|
||||
// For magnitudes >1, move one month at a time...
|
||||
for (var i=0; i<mag; i++)
|
||||
// ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
|
||||
new_date = this.moveMonth(new_date, dir);
|
||||
// ...then reset the day, keeping it in the new month
|
||||
new_month = new_date.getUTCMonth();
|
||||
new_date.setUTCDate(day);
|
||||
test = function(){ return new_month != new_date.getUTCMonth(); };
|
||||
}
|
||||
// Common date-resetting loop -- if date is beyond end of month, make it
|
||||
// end of month
|
||||
while (test()){
|
||||
new_date.setUTCDate(--day);
|
||||
new_date.setUTCMonth(new_month);
|
||||
}
|
||||
return new_date;
|
||||
},
|
||||
|
||||
moveYear: function(date, dir){
|
||||
return this.moveMonth(date, dir*12);
|
||||
},
|
||||
|
||||
dateWithinRange: function(date){
|
||||
return date >= this.startDate && date <= this.endDate;
|
||||
},
|
||||
|
||||
keydown: function(e){
|
||||
if (this.picker.is(':not(:visible)')){
|
||||
if (e.keyCode == 27) // allow escape to hide and re-show picker
|
||||
this.show();
|
||||
return;
|
||||
}
|
||||
var dateChanged = false,
|
||||
dir, day, month,
|
||||
newDate, newViewDate;
|
||||
switch(e.keyCode){
|
||||
case 27: // escape
|
||||
this.hide();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 37: // left
|
||||
case 39: // right
|
||||
if (!this.keyboardNavigation) break;
|
||||
dir = e.keyCode == 37 ? -1 : 1;
|
||||
if (e.ctrlKey){
|
||||
newDate = this.moveYear(this.date, dir);
|
||||
newViewDate = this.moveYear(this.viewDate, dir);
|
||||
} else if (e.shiftKey){
|
||||
newDate = this.moveMonth(this.date, dir);
|
||||
newViewDate = this.moveMonth(this.viewDate, dir);
|
||||
} else {
|
||||
newDate = new Date(this.date);
|
||||
newDate.setUTCDate(this.date.getUTCDate() + dir);
|
||||
newViewDate = new Date(this.viewDate);
|
||||
newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir);
|
||||
}
|
||||
if (this.dateWithinRange(newDate)){
|
||||
this.date = newDate;
|
||||
this.viewDate = newViewDate;
|
||||
this.setValue();
|
||||
this.update();
|
||||
e.preventDefault();
|
||||
dateChanged = true;
|
||||
}
|
||||
break;
|
||||
case 38: // up
|
||||
case 40: // down
|
||||
if (!this.keyboardNavigation) break;
|
||||
dir = e.keyCode == 38 ? -1 : 1;
|
||||
if (e.ctrlKey){
|
||||
newDate = this.moveYear(this.date, dir);
|
||||
newViewDate = this.moveYear(this.viewDate, dir);
|
||||
} else if (e.shiftKey){
|
||||
newDate = this.moveMonth(this.date, dir);
|
||||
newViewDate = this.moveMonth(this.viewDate, dir);
|
||||
} else {
|
||||
newDate = new Date(this.date);
|
||||
newDate.setUTCDate(this.date.getUTCDate() + dir * 7);
|
||||
newViewDate = new Date(this.viewDate);
|
||||
newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7);
|
||||
}
|
||||
if (this.dateWithinRange(newDate)){
|
||||
this.date = newDate;
|
||||
this.viewDate = newViewDate;
|
||||
this.setValue();
|
||||
this.update();
|
||||
e.preventDefault();
|
||||
dateChanged = true;
|
||||
}
|
||||
break;
|
||||
case 13: // enter
|
||||
this.hide();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 9: // tab
|
||||
this.hide();
|
||||
break;
|
||||
}
|
||||
if (dateChanged){
|
||||
this.element.trigger({
|
||||
type: 'changeDate',
|
||||
date: this.date
|
||||
});
|
||||
var element;
|
||||
if (this.isInput) {
|
||||
element = this.element;
|
||||
} else if (this.component){
|
||||
element = this.element.find('input');
|
||||
}
|
||||
if (element) {
|
||||
element.change();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
showMode: function(dir) {
|
||||
if (dir) {
|
||||
this.viewMode = Math.max(0, Math.min(2, this.viewMode + dir));
|
||||
}
|
||||
/*
|
||||
vitalets: fixing bug of very special conditions:
|
||||
jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
|
||||
Method show() does not set display css correctly and datepicker is not shown.
|
||||
Changed to .css('display', 'block') solve the problem.
|
||||
See https://github.com/vitalets/x-editable/issues/37
|
||||
|
||||
In jquery 1.7.2+ everything works fine.
|
||||
*/
|
||||
//this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
|
||||
this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
|
||||
this.updateNavArrows();
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.datepicker = function ( option ) {
|
||||
var args = Array.apply(null, arguments);
|
||||
args.shift();
|
||||
return this.each(function () {
|
||||
var $this = $(this),
|
||||
data = $this.data('datepicker'),
|
||||
options = typeof option == 'object' && option;
|
||||
if (!data) {
|
||||
$this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options))));
|
||||
}
|
||||
if (typeof option == 'string' && typeof data[option] == 'function') {
|
||||
data[option].apply(data, args);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.datepicker.defaults = {
|
||||
};
|
||||
$.fn.datepicker.Constructor = Datepicker;
|
||||
var dates = $.fn.datepicker.dates = {
|
||||
en: {
|
||||
days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
|
||||
daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
|
||||
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
||||
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
today: "Today"
|
||||
}
|
||||
};
|
||||
|
||||
var DPGlobal = {
|
||||
modes: [
|
||||
{
|
||||
clsName: 'days',
|
||||
navFnc: 'Month',
|
||||
navStep: 1
|
||||
},
|
||||
{
|
||||
clsName: 'months',
|
||||
navFnc: 'FullYear',
|
||||
navStep: 1
|
||||
},
|
||||
{
|
||||
clsName: 'years',
|
||||
navFnc: 'FullYear',
|
||||
navStep: 10
|
||||
}],
|
||||
isLeapYear: function (year) {
|
||||
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
|
||||
},
|
||||
getDaysInMonth: function (year, month) {
|
||||
return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
|
||||
},
|
||||
validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
|
||||
nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
|
||||
parseFormat: function(format){
|
||||
// IE treats \0 as a string end in inputs (truncating the value),
|
||||
// so it's a bad format delimiter, anyway
|
||||
var separators = format.replace(this.validParts, '\0').split('\0'),
|
||||
parts = format.match(this.validParts);
|
||||
if (!separators || !separators.length || !parts || parts.length == 0){
|
||||
throw new Error("Invalid date format.");
|
||||
}
|
||||
return {separators: separators, parts: parts};
|
||||
},
|
||||
parseDate: function(date, format, language) {
|
||||
if (date instanceof Date) return date;
|
||||
if (/^[-+]\d+[dmwy]([\s,]+[-+]\d+[dmwy])*$/.test(date)) {
|
||||
var part_re = /([-+]\d+)([dmwy])/,
|
||||
parts = date.match(/([-+]\d+)([dmwy])/g),
|
||||
part, dir;
|
||||
date = new Date();
|
||||
for (var i=0; i<parts.length; i++) {
|
||||
part = part_re.exec(parts[i]);
|
||||
dir = parseInt(part[1]);
|
||||
switch(part[2]){
|
||||
case 'd':
|
||||
date.setUTCDate(date.getUTCDate() + dir);
|
||||
break;
|
||||
case 'm':
|
||||
date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir);
|
||||
break;
|
||||
case 'w':
|
||||
date.setUTCDate(date.getUTCDate() + dir * 7);
|
||||
break;
|
||||
case 'y':
|
||||
date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
|
||||
}
|
||||
var parts = date && date.match(this.nonpunctuation) || [],
|
||||
date = new Date(),
|
||||
parsed = {},
|
||||
setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
|
||||
setters_map = {
|
||||
yyyy: function(d,v){ return d.setUTCFullYear(v); },
|
||||
yy: function(d,v){ return d.setUTCFullYear(2000+v); },
|
||||
m: function(d,v){
|
||||
v -= 1;
|
||||
while (v<0) v += 12;
|
||||
v %= 12;
|
||||
d.setUTCMonth(v);
|
||||
while (d.getUTCMonth() != v)
|
||||
d.setUTCDate(d.getUTCDate()-1);
|
||||
return d;
|
||||
},
|
||||
d: function(d,v){ return d.setUTCDate(v); }
|
||||
},
|
||||
val, filtered, part;
|
||||
setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
|
||||
setters_map['dd'] = setters_map['d'];
|
||||
date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
|
||||
var fparts = format.parts.slice();
|
||||
// Remove noop parts
|
||||
if (parts.length != fparts.length) {
|
||||
fparts = $(fparts).filter(function(i,p){
|
||||
return $.inArray(p, setters_order) !== -1;
|
||||
}).toArray();
|
||||
}
|
||||
// Process remainder
|
||||
if (parts.length == fparts.length) {
|
||||
for (var i=0, cnt = fparts.length; i < cnt; i++) {
|
||||
val = parseInt(parts[i], 10);
|
||||
part = fparts[i];
|
||||
if (isNaN(val)) {
|
||||
switch(part) {
|
||||
case 'MM':
|
||||
filtered = $(dates[language].months).filter(function(){
|
||||
var m = this.slice(0, parts[i].length),
|
||||
p = parts[i].slice(0, m.length);
|
||||
return m == p;
|
||||
});
|
||||
val = $.inArray(filtered[0], dates[language].months) + 1;
|
||||
break;
|
||||
case 'M':
|
||||
filtered = $(dates[language].monthsShort).filter(function(){
|
||||
var m = this.slice(0, parts[i].length),
|
||||
p = parts[i].slice(0, m.length);
|
||||
return m == p;
|
||||
});
|
||||
val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
parsed[part] = val;
|
||||
}
|
||||
for (var i=0, s; i<setters_order.length; i++){
|
||||
s = setters_order[i];
|
||||
if (s in parsed && !isNaN(parsed[s]))
|
||||
setters_map[s](date, parsed[s])
|
||||
}
|
||||
}
|
||||
return date;
|
||||
},
|
||||
formatDate: function(date, format, language){
|
||||
var val = {
|
||||
d: date.getUTCDate(),
|
||||
D: dates[language].daysShort[date.getUTCDay()],
|
||||
DD: dates[language].days[date.getUTCDay()],
|
||||
m: date.getUTCMonth() + 1,
|
||||
M: dates[language].monthsShort[date.getUTCMonth()],
|
||||
MM: dates[language].months[date.getUTCMonth()],
|
||||
yy: date.getUTCFullYear().toString().substring(2),
|
||||
yyyy: date.getUTCFullYear()
|
||||
};
|
||||
val.dd = (val.d < 10 ? '0' : '') + val.d;
|
||||
val.mm = (val.m < 10 ? '0' : '') + val.m;
|
||||
var date = [],
|
||||
seps = $.extend([], format.separators);
|
||||
for (var i=0, cnt = format.parts.length; i < cnt; i++) {
|
||||
if (seps.length)
|
||||
date.push(seps.shift())
|
||||
date.push(val[format.parts[i]]);
|
||||
}
|
||||
return date.join('');
|
||||
},
|
||||
headTemplate: '<thead>'+
|
||||
'<tr>'+
|
||||
'<th class="prev"><i class="fas fa-arrow-left"/></th>'+
|
||||
'<th colspan="5" class="switch"></th>'+
|
||||
'<th class="next"><i class="fas fa-arrow-right"/></th>'+
|
||||
'</tr>'+
|
||||
'</thead>',
|
||||
contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>',
|
||||
footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr></tfoot>'
|
||||
};
|
||||
DPGlobal.template = '<div class="datepicker">'+
|
||||
'<div class="datepicker-days">'+
|
||||
'<table class=" table-condensed">'+
|
||||
DPGlobal.headTemplate+
|
||||
'<tbody></tbody>'+
|
||||
DPGlobal.footTemplate+
|
||||
'</table>'+
|
||||
'</div>'+
|
||||
'<div class="datepicker-months">'+
|
||||
'<table class="table-condensed">'+
|
||||
DPGlobal.headTemplate+
|
||||
DPGlobal.contTemplate+
|
||||
DPGlobal.footTemplate+
|
||||
'</table>'+
|
||||
'</div>'+
|
||||
'<div class="datepicker-years">'+
|
||||
'<table class="table-condensed">'+
|
||||
DPGlobal.headTemplate+
|
||||
DPGlobal.contTemplate+
|
||||
DPGlobal.footTemplate+
|
||||
'</table>'+
|
||||
'</div>'+
|
||||
'</div>';
|
||||
|
||||
$.fn.datepicker.DPGlobal = DPGlobal;
|
||||
|
||||
}( window.jQuery );
|
||||
804
static/js/bootstrap-timepicker.js
vendored
804
static/js/bootstrap-timepicker.js
vendored
@@ -1,804 +0,0 @@
|
||||
/* =========================================================
|
||||
* bootstrap-timepicker.js
|
||||
* http://www.github.com/jdewit/bootstrap-timepicker
|
||||
* =========================================================
|
||||
* Copyright 2012
|
||||
*
|
||||
* Created By:
|
||||
* Joris de Wit @joris_dewit
|
||||
*
|
||||
* Contributions By:
|
||||
* Gilbert @mindeavor
|
||||
* Koen Punt info@koenpunt.nl
|
||||
* Nek
|
||||
* Chris Martin
|
||||
* Dominic Barnes contact@dominicbarnes.us
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* ========================================================= */
|
||||
|
||||
!function($) {
|
||||
|
||||
"use strict"; // jshint ;_;
|
||||
|
||||
/* TIMEPICKER PUBLIC CLASS DEFINITION
|
||||
* ================================== */
|
||||
var Timepicker = function(element, options) {
|
||||
this.$element = $(element);
|
||||
this.options = $.extend({}, $.fn.timepicker.defaults, options, this.$element.data());
|
||||
this.minuteStep = this.options.minuteStep || this.minuteStep;
|
||||
this.secondStep = this.options.secondStep || this.secondStep;
|
||||
this.showMeridian = this.options.showMeridian || this.showMeridian;
|
||||
this.showSeconds = this.options.showSeconds || this.showSeconds;
|
||||
this.showInputs = this.options.showInputs || this.showInputs;
|
||||
this.disableFocus = this.options.disableFocus || this.disableFocus;
|
||||
this.template = this.options.template || this.template;
|
||||
this.modalBackdrop = this.options.modalBackdrop || this.modalBackdrop;
|
||||
this.defaultTime = this.options.defaultTime || this.defaultTime;
|
||||
this.open = false;
|
||||
this.init();
|
||||
};
|
||||
|
||||
Timepicker.prototype = {
|
||||
|
||||
constructor: Timepicker
|
||||
|
||||
, init: function () {
|
||||
if (this.$element.parent().hasClass('input-append')) {
|
||||
this.$element.parent('.input-append').find('.add-on').on('click', $.proxy(this.showWidget, this));
|
||||
this.$element.on({
|
||||
focus: $.proxy(this.highlightUnit, this),
|
||||
click: $.proxy(this.highlightUnit, this),
|
||||
keypress: $.proxy(this.elementKeypress, this),
|
||||
blur: $.proxy(this.blurElement, this)
|
||||
});
|
||||
|
||||
} else {
|
||||
if (this.template) {
|
||||
this.$element.on({
|
||||
focus: $.proxy(this.showWidget, this),
|
||||
click: $.proxy(this.showWidget, this),
|
||||
blur: $.proxy(this.blurElement, this)
|
||||
});
|
||||
} else {
|
||||
this.$element.on({
|
||||
focus: $.proxy(this.highlightUnit, this),
|
||||
click: $.proxy(this.highlightUnit, this),
|
||||
keypress: $.proxy(this.elementKeypress, this),
|
||||
blur: $.proxy(this.blurElement, this)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.$widget = $(this.getTemplate()).appendTo('body');
|
||||
|
||||
this.$widget.on('click', $.proxy(this.widgetClick, this));
|
||||
|
||||
if (this.showInputs) {
|
||||
this.$widget.find('input').on({
|
||||
click: function() { this.select(); },
|
||||
keypress: $.proxy(this.widgetKeypress, this),
|
||||
change: $.proxy(this.updateFromWidgetInputs, this)
|
||||
});
|
||||
}
|
||||
|
||||
this.setDefaultTime(this.defaultTime);
|
||||
}
|
||||
|
||||
, showWidget: function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$element.trigger('show');
|
||||
|
||||
if (this.disableFocus) {
|
||||
this.$element.blur();
|
||||
}
|
||||
|
||||
var pos = $.extend({}, this.$element.offset(), {
|
||||
height: this.$element[0].offsetHeight
|
||||
});
|
||||
|
||||
this.updateFromElementVal();
|
||||
|
||||
$('html')
|
||||
.trigger('click.timepicker.data-api')
|
||||
.one('click.timepicker.data-api', $.proxy(this.hideWidget, this));
|
||||
|
||||
if (this.template === 'modal') {
|
||||
this.$widget.modal('show').on('hidden', $.proxy(this.hideWidget, this));
|
||||
} else {
|
||||
this.$widget.css({
|
||||
top: pos.top + pos.height
|
||||
, left: pos.left
|
||||
})
|
||||
|
||||
if (!this.open) {
|
||||
this.$widget.addClass('open');
|
||||
}
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
this.$element.trigger('shown');
|
||||
}
|
||||
|
||||
, hideWidget: function(){
|
||||
this.$element.trigger('hide');
|
||||
|
||||
if (this.template === 'modal') {
|
||||
this.$widget.modal('hide');
|
||||
} else {
|
||||
this.$widget.removeClass('open');
|
||||
}
|
||||
this.open = false;
|
||||
this.$element.trigger('hidden');
|
||||
}
|
||||
|
||||
, widgetClick: function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
var action = $(e.target).closest('a').data('action');
|
||||
if (action) {
|
||||
this[action]();
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
, widgetKeypress: function(e) {
|
||||
var input = $(e.target).closest('input').attr('name');
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 9: //tab
|
||||
if (this.showMeridian) {
|
||||
if (input == 'meridian') {
|
||||
this.hideWidget();
|
||||
}
|
||||
} else {
|
||||
if (this.showSeconds) {
|
||||
if (input == 'second') {
|
||||
this.hideWidget();
|
||||
}
|
||||
} else {
|
||||
if (input == 'minute') {
|
||||
this.hideWidget();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 27: // escape
|
||||
this.hideWidget();
|
||||
break;
|
||||
case 38: // up arrow
|
||||
switch (input) {
|
||||
case 'hour':
|
||||
this.incrementHour();
|
||||
break;
|
||||
case 'minute':
|
||||
this.incrementMinute();
|
||||
break;
|
||||
case 'second':
|
||||
this.incrementSecond();
|
||||
break;
|
||||
case 'meridian':
|
||||
this.toggleMeridian();
|
||||
break;
|
||||
}
|
||||
this.update();
|
||||
break;
|
||||
case 40: // down arrow
|
||||
switch (input) {
|
||||
case 'hour':
|
||||
this.decrementHour();
|
||||
break;
|
||||
case 'minute':
|
||||
this.decrementMinute();
|
||||
break;
|
||||
case 'second':
|
||||
this.decrementSecond();
|
||||
break;
|
||||
case 'meridian':
|
||||
this.toggleMeridian();
|
||||
break;
|
||||
}
|
||||
this.update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
, elementKeypress: function(e) {
|
||||
var input = this.$element.get(0);
|
||||
switch (e.keyCode) {
|
||||
case 0: //input
|
||||
break;
|
||||
case 9: //tab
|
||||
this.updateFromElementVal();
|
||||
if (this.showMeridian) {
|
||||
if (this.highlightedUnit != 'meridian') {
|
||||
e.preventDefault();
|
||||
this.highlightNextUnit();
|
||||
}
|
||||
} else {
|
||||
if (this.showSeconds) {
|
||||
if (this.highlightedUnit != 'second') {
|
||||
e.preventDefault();
|
||||
this.highlightNextUnit();
|
||||
}
|
||||
} else {
|
||||
if (this.highlightedUnit != 'minute') {
|
||||
e.preventDefault();
|
||||
this.highlightNextUnit();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 27: // escape
|
||||
this.updateFromElementVal();
|
||||
break;
|
||||
case 37: // left arrow
|
||||
this.updateFromElementVal();
|
||||
this.highlightPrevUnit();
|
||||
break;
|
||||
case 38: // up arrow
|
||||
switch (this.highlightedUnit) {
|
||||
case 'hour':
|
||||
this.incrementHour();
|
||||
break;
|
||||
case 'minute':
|
||||
this.incrementMinute();
|
||||
break;
|
||||
case 'second':
|
||||
this.incrementSecond();
|
||||
break;
|
||||
case 'meridian':
|
||||
this.toggleMeridian();
|
||||
break;
|
||||
}
|
||||
this.updateElement();
|
||||
break;
|
||||
case 39: // right arrow
|
||||
this.updateFromElementVal();
|
||||
this.highlightNextUnit();
|
||||
break;
|
||||
case 40: // down arrow
|
||||
switch (this.highlightedUnit) {
|
||||
case 'hour':
|
||||
this.decrementHour();
|
||||
break;
|
||||
case 'minute':
|
||||
this.decrementMinute();
|
||||
break;
|
||||
case 'second':
|
||||
this.decrementSecond();
|
||||
break;
|
||||
case 'meridian':
|
||||
this.toggleMeridian();
|
||||
break;
|
||||
}
|
||||
this.updateElement();
|
||||
break;
|
||||
}
|
||||
|
||||
if (e.keyCode !== 0 && e.keyCode !== 8 && e.keyCode !== 9 && e.keyCode !== 46) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
, setValues: function(time) {
|
||||
if (this.showMeridian) {
|
||||
var arr = time.split(' ');
|
||||
var timeArray = arr[0].split(':');
|
||||
this.meridian = arr[1];
|
||||
} else {
|
||||
var timeArray = time.split(':');
|
||||
}
|
||||
|
||||
this.hour = parseInt(timeArray[0], 10);
|
||||
this.minute = parseInt(timeArray[1], 10);
|
||||
this.second = parseInt(timeArray[2], 10);
|
||||
|
||||
if (isNaN(this.hour)) {
|
||||
this.hour = 0;
|
||||
}
|
||||
if (isNaN(this.minute)) {
|
||||
this.minute = 0;
|
||||
}
|
||||
|
||||
if (this.showMeridian) {
|
||||
if (this.hour > 12) {
|
||||
this.hour = 12;
|
||||
} else if (this.hour < 1) {
|
||||
this.hour = 1;
|
||||
}
|
||||
|
||||
if (this.meridian == 'am' || this.meridian == 'a') {
|
||||
this.meridian = 'AM';
|
||||
} else if (this.meridian == 'pm' || this.meridian == 'p') {
|
||||
this.meridian = 'PM';
|
||||
}
|
||||
|
||||
if (this.meridian != 'AM' && this.meridian != 'PM') {
|
||||
this.meridian = 'AM';
|
||||
}
|
||||
} else {
|
||||
if (this.hour >= 24) {
|
||||
this.hour = 23;
|
||||
} else if (this.hour < 0) {
|
||||
this.hour = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.minute < 0) {
|
||||
this.minute = 0;
|
||||
} else if (this.minute >= 60) {
|
||||
this.minute = 59;
|
||||
}
|
||||
|
||||
if (this.showSeconds) {
|
||||
if (isNaN(this.second)) {
|
||||
this.second = 0;
|
||||
} else if (this.second < 0) {
|
||||
this.second = 0;
|
||||
} else if (this.second >= 60) {
|
||||
this.second = 59;
|
||||
}
|
||||
}
|
||||
|
||||
if ( this.$element.val() != '' )
|
||||
this.updateElement();
|
||||
this.updateWidget();
|
||||
}
|
||||
|
||||
, setMeridian: function(meridian) {
|
||||
if (meridian == 'a' || meridian == 'am' || meridian == 'AM' ) {
|
||||
this.meridian = 'AM';
|
||||
} else if (meridian == 'p' || meridian == 'pm' || meridian == 'PM' ) {
|
||||
this.meridian = 'PM';
|
||||
} else {
|
||||
this.updateWidget();
|
||||
}
|
||||
|
||||
this.updateElement();
|
||||
}
|
||||
|
||||
, setDefaultTime: function(defaultTime){
|
||||
if (defaultTime) {
|
||||
if (defaultTime === 'current') {
|
||||
var dTime = new Date();
|
||||
var hours = dTime.getHours();
|
||||
var minutes = Math.floor(dTime.getMinutes() / this.minuteStep) * this.minuteStep;
|
||||
var seconds = Math.floor(dTime.getSeconds() / this.secondStep) * this.secondStep;
|
||||
var meridian = "AM";
|
||||
if (this.showMeridian) {
|
||||
if (hours === 0) {
|
||||
hours = 12;
|
||||
} else if (hours >= 12) {
|
||||
if (hours > 12) {
|
||||
hours = hours - 12;
|
||||
}
|
||||
meridian = "PM";
|
||||
} else {
|
||||
meridian = "AM";
|
||||
}
|
||||
}
|
||||
this.hour = hours;
|
||||
this.minute = minutes;
|
||||
this.second = seconds;
|
||||
this.meridian = meridian;
|
||||
} else if (defaultTime === 'value') {
|
||||
this.setValues(this.$element.val());
|
||||
} else {
|
||||
this.setValues(defaultTime);
|
||||
}
|
||||
if ( this.$element.val() != '' )
|
||||
this.updateElement();
|
||||
this.updateWidget();
|
||||
this.updateElement();
|
||||
} else {
|
||||
this.hour = 0;
|
||||
this.minute = 0;
|
||||
this.second = 0;
|
||||
}
|
||||
}
|
||||
|
||||
, formatTime: function(hour, minute, second, meridian) {
|
||||
hour = hour < 10 ? '0' + hour : hour;
|
||||
minute = minute < 10 ? '0' + minute : minute;
|
||||
second = second < 10 ? '0' + second : second;
|
||||
|
||||
return hour + ':' + minute + (this.showSeconds ? ':' + second : '') + (this.showMeridian ? ' ' + meridian : '');
|
||||
}
|
||||
|
||||
, getTime: function() {
|
||||
return this.formatTime(this.hour, this.minute, this.second, this.meridian);
|
||||
}
|
||||
|
||||
, setTime: function(time) {
|
||||
this.setValues(time);
|
||||
this.update();
|
||||
}
|
||||
|
||||
, update: function() {
|
||||
this.updateElement();
|
||||
this.updateWidget();
|
||||
}
|
||||
|
||||
, blurElement: function() {
|
||||
this.highlightedUnit = undefined;
|
||||
this.updateFromElementVal();
|
||||
}
|
||||
|
||||
, updateElement: function() {
|
||||
var time = this.getTime();
|
||||
|
||||
this.$element.val(time).change();
|
||||
|
||||
switch (this.highlightedUnit) {
|
||||
case 'hour':
|
||||
this.highlightHour();
|
||||
break;
|
||||
case 'minute':
|
||||
this.highlightMinute();
|
||||
break;
|
||||
case 'second':
|
||||
this.highlightSecond();
|
||||
break;
|
||||
case 'meridian':
|
||||
this.highlightMeridian();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
, updateWidget: function() {
|
||||
if (this.showInputs) {
|
||||
this.$widget.find('input.bootstrap-timepicker-hour').val(this.hour < 10 ? '0' + this.hour : this.hour);
|
||||
this.$widget.find('input.bootstrap-timepicker-minute').val(this.minute < 10 ? '0' + this.minute : this.minute);
|
||||
if (this.showSeconds) {
|
||||
this.$widget.find('input.bootstrap-timepicker-second').val(this.second < 10 ? '0' + this.second : this.second);
|
||||
}
|
||||
if (this.showMeridian) {
|
||||
this.$widget.find('input.bootstrap-timepicker-meridian').val(this.meridian);
|
||||
}
|
||||
} else {
|
||||
this.$widget.find('span.bootstrap-timepicker-hour').text(this.hour);
|
||||
this.$widget.find('span.bootstrap-timepicker-minute').text(this.minute < 10 ? '0' + this.minute : this.minute);
|
||||
if (this.showSeconds) {
|
||||
this.$widget.find('span.bootstrap-timepicker-second').text(this.second < 10 ? '0' + this.second : this.second);
|
||||
}
|
||||
if (this.showMeridian) {
|
||||
this.$widget.find('span.bootstrap-timepicker-meridian').text(this.meridian);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, updateFromElementVal: function (e) {
|
||||
var time = this.$element.val();
|
||||
if (time) {
|
||||
this.setValues(time);
|
||||
this.updateWidget();
|
||||
}
|
||||
}
|
||||
|
||||
, updateFromWidgetInputs: function () {
|
||||
var time = $('input.bootstrap-timepicker-hour', this.$widget).val() + ':' +
|
||||
$('input.bootstrap-timepicker-minute', this.$widget).val() +
|
||||
(this.showSeconds ?
|
||||
':' + $('input.bootstrap-timepicker-second', this.$widget).val()
|
||||
: '') +
|
||||
(this.showMeridian ?
|
||||
' ' + $('input.bootstrap-timepicker-meridian', this.$widget).val()
|
||||
: '');
|
||||
|
||||
this.setValues(time);
|
||||
}
|
||||
|
||||
, getCursorPosition: function() {
|
||||
var input = this.$element.get(0);
|
||||
|
||||
if ('selectionStart' in input) {
|
||||
// Standard-compliant browsers
|
||||
return input.selectionStart;
|
||||
} else if (document.selection) {
|
||||
// IE fix
|
||||
input.focus();
|
||||
var sel = document.selection.createRange();
|
||||
var selLen = document.selection.createRange().text.length;
|
||||
sel.moveStart('character', - input.value.length);
|
||||
|
||||
return sel.text.length - selLen;
|
||||
}
|
||||
}
|
||||
|
||||
, highlightUnit: function () {
|
||||
var input = this.$element.get(0);
|
||||
|
||||
this.position = this.getCursorPosition();
|
||||
if (this.position >= 0 && this.position <= 2) {
|
||||
this.highlightHour();
|
||||
} else if (this.position >= 3 && this.position <= 5) {
|
||||
this.highlightMinute();
|
||||
} else if (this.position >= 6 && this.position <= 8) {
|
||||
if (this.showSeconds) {
|
||||
this.highlightSecond();
|
||||
} else {
|
||||
this.highlightMeridian();
|
||||
}
|
||||
} else if (this.position >= 9 && this.position <= 11) {
|
||||
this.highlightMeridian();
|
||||
}
|
||||
}
|
||||
|
||||
, highlightNextUnit: function() {
|
||||
switch (this.highlightedUnit) {
|
||||
case 'hour':
|
||||
this.highlightMinute();
|
||||
break;
|
||||
case 'minute':
|
||||
if (this.showSeconds) {
|
||||
this.highlightSecond();
|
||||
} else {
|
||||
this.highlightMeridian();
|
||||
}
|
||||
break;
|
||||
case 'second':
|
||||
this.highlightMeridian();
|
||||
break;
|
||||
case 'meridian':
|
||||
this.highlightHour();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
, highlightPrevUnit: function() {
|
||||
switch (this.highlightedUnit) {
|
||||
case 'hour':
|
||||
this.highlightMeridian();
|
||||
break;
|
||||
case 'minute':
|
||||
this.highlightHour();
|
||||
break;
|
||||
case 'second':
|
||||
this.highlightMinute();
|
||||
break;
|
||||
case 'meridian':
|
||||
if (this.showSeconds) {
|
||||
this.highlightSecond();
|
||||
} else {
|
||||
this.highlightMinute();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
, highlightHour: function() {
|
||||
this.highlightedUnit = 'hour';
|
||||
this.$element.get(0).setSelectionRange(0,2);
|
||||
}
|
||||
|
||||
, highlightMinute: function() {
|
||||
this.highlightedUnit = 'minute';
|
||||
this.$element.get(0).setSelectionRange(3,5);
|
||||
}
|
||||
|
||||
, highlightSecond: function() {
|
||||
this.highlightedUnit = 'second';
|
||||
this.$element.get(0).setSelectionRange(6,8);
|
||||
}
|
||||
|
||||
, highlightMeridian: function() {
|
||||
this.highlightedUnit = 'meridian';
|
||||
if (this.showSeconds) {
|
||||
this.$element.get(0).setSelectionRange(9,11);
|
||||
} else {
|
||||
this.$element.get(0).setSelectionRange(6,8);
|
||||
}
|
||||
}
|
||||
|
||||
, incrementHour: function() {
|
||||
if (this.showMeridian) {
|
||||
if (this.hour === 11) {
|
||||
this.toggleMeridian();
|
||||
} else if (this.hour === 12) {
|
||||
return this.hour = 1;
|
||||
}
|
||||
}
|
||||
if (this.hour === 23) {
|
||||
return this.hour = 0;
|
||||
}
|
||||
this.hour = this.hour + 1;
|
||||
}
|
||||
|
||||
, decrementHour: function() {
|
||||
if (this.showMeridian) {
|
||||
if (this.hour === 1) {
|
||||
return this.hour = 12;
|
||||
}
|
||||
else if (this.hour === 12) {
|
||||
this.toggleMeridian();
|
||||
}
|
||||
}
|
||||
if (this.hour === 0) {
|
||||
return this.hour = 23;
|
||||
}
|
||||
this.hour = this.hour - 1;
|
||||
}
|
||||
|
||||
, incrementMinute: function() {
|
||||
var newVal = this.minute + this.minuteStep - (this.minute % this.minuteStep);
|
||||
if (newVal > 59) {
|
||||
this.incrementHour();
|
||||
this.minute = newVal - 60;
|
||||
} else {
|
||||
this.minute = newVal;
|
||||
}
|
||||
}
|
||||
|
||||
, decrementMinute: function() {
|
||||
var newVal = this.minute - this.minuteStep;
|
||||
if (newVal < 0) {
|
||||
this.decrementHour();
|
||||
this.minute = newVal + 60;
|
||||
} else {
|
||||
this.minute = newVal;
|
||||
}
|
||||
}
|
||||
|
||||
, incrementSecond: function() {
|
||||
var newVal = this.second + this.secondStep - (this.second % this.secondStep);
|
||||
if (newVal > 59) {
|
||||
this.incrementMinute();
|
||||
this.second = newVal - 60;
|
||||
} else {
|
||||
this.second = newVal;
|
||||
}
|
||||
}
|
||||
|
||||
, decrementSecond: function() {
|
||||
var newVal = this.second - this.secondStep;
|
||||
if (newVal < 0) {
|
||||
this.decrementMinute();
|
||||
this.second = newVal + 60;
|
||||
} else {
|
||||
this.second = newVal;
|
||||
}
|
||||
}
|
||||
|
||||
, toggleMeridian: function() {
|
||||
this.meridian = this.meridian === 'AM' ? 'PM' : 'AM';
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
, getTemplate: function() {
|
||||
if (this.options.templates[this.options.template]) {
|
||||
return this.options.templates[this.options.template];
|
||||
}
|
||||
if (this.showInputs) {
|
||||
var hourTemplate = '<input type="text" name="hour" class="bootstrap-timepicker-hour" maxlength="2"/>';
|
||||
var minuteTemplate = '<input type="text" name="minute" class="bootstrap-timepicker-minute" maxlength="2"/>';
|
||||
var secondTemplate = '<input type="text" name="second" class="bootstrap-timepicker-second" maxlength="2"/>';
|
||||
var meridianTemplate = '<input type="text" name="meridian" class="bootstrap-timepicker-meridian" maxlength="2"/>';
|
||||
} else {
|
||||
var hourTemplate = '<span class="bootstrap-timepicker-hour"></span>';
|
||||
var minuteTemplate = '<span class="bootstrap-timepicker-minute"></span>';
|
||||
var secondTemplate = '<span class="bootstrap-timepicker-second"></span>';
|
||||
var meridianTemplate = '<span class="bootstrap-timepicker-meridian"></span>';
|
||||
}
|
||||
var templateContent = '<table class="'+ (this.showSeconds ? 'show-seconds' : '') +' '+ (this.showMeridian ? 'show-meridian' : '') +'">'+
|
||||
'<tr>'+
|
||||
'<td><a href="#" data-action="incrementHour"><i class="fas fa-chevron-up"></i></a></td>'+
|
||||
'<td class="separator"> </td>'+
|
||||
'<td><a href="#" data-action="incrementMinute"><i class="fas fa-chevron-up"></i></a></td>'+
|
||||
(this.showSeconds ?
|
||||
'<td class="separator"> </td>'+
|
||||
'<td><a href="#" data-action="incrementSecond"><i class="fas fa-chevron-up"></i></a></td>'
|
||||
: '') +
|
||||
(this.showMeridian ?
|
||||
'<td class="separator"> </td>'+
|
||||
'<td class="meridian-column"><a href="#" data-action="toggleMeridian"><i class="fas fa-chevron-up"></i></a></td>'
|
||||
: '') +
|
||||
'</tr>'+
|
||||
'<tr>'+
|
||||
'<td>'+ hourTemplate +'</td> '+
|
||||
'<td class="separator">:</td>'+
|
||||
'<td>'+ minuteTemplate +'</td> '+
|
||||
(this.showSeconds ?
|
||||
'<td class="separator">:</td>'+
|
||||
'<td>'+ secondTemplate +'</td>'
|
||||
: '') +
|
||||
(this.showMeridian ?
|
||||
'<td class="separator"> </td>'+
|
||||
'<td>'+ meridianTemplate +'</td>'
|
||||
: '') +
|
||||
'</tr>'+
|
||||
'<tr>'+
|
||||
'<td><a href="#" data-action="decrementHour"><i class="fas fa-chevron-down"></i></a></td>'+
|
||||
'<td class="separator"></td>'+
|
||||
'<td><a href="#" data-action="decrementMinute"><i class="fas fa-chevron-down"></i></a></td>'+
|
||||
(this.showSeconds ?
|
||||
'<td class="separator"> </td>'+
|
||||
'<td><a href="#" data-action="decrementSecond"><i class="fas fa-chevron-down"></i></a></td>'
|
||||
: '') +
|
||||
(this.showMeridian ?
|
||||
'<td class="separator"> </td>'+
|
||||
'<td><a href="#" data-action="toggleMeridian"><i class="fas fa-chevron-down"></i></a></td>'
|
||||
: '') +
|
||||
'</tr>'+
|
||||
'</table>';
|
||||
|
||||
var template;
|
||||
switch(this.options.template) {
|
||||
case 'modal':
|
||||
template = '<div class="bootstrap-timepicker modal hide fade in" style="top: 30%; margin-top: 0; width: 200px; margin-left: -100px;" data-backdrop="'+ (this.modalBackdrop ? 'true' : 'false') +'">'+
|
||||
'<div class="modal-header">'+
|
||||
'<a href="#" class="close" data-dismiss="modal">×</a>'+
|
||||
'<h3>Pick a Time</h3>'+
|
||||
'</div>'+
|
||||
'<div class="modal-content">'+
|
||||
templateContent +
|
||||
'</div>'+
|
||||
'<div class="modal-footer">'+
|
||||
'<a href="#" class="btn btn-primary" data-dismiss="modal">Ok</a>'+
|
||||
'</div>'+
|
||||
'</div>';
|
||||
|
||||
break;
|
||||
case 'dropdown':
|
||||
template = '<div class="bootstrap-timepicker dropdown-menu">'+
|
||||
templateContent +
|
||||
'</div>';
|
||||
break;
|
||||
|
||||
}
|
||||
return template;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* TIMEPICKER PLUGIN DEFINITION
|
||||
* =========================== */
|
||||
|
||||
$.fn.timepicker = function (option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
, data = $this.data('timepicker')
|
||||
, options = typeof option == 'object' && option;
|
||||
if (!data) {
|
||||
$this.data('timepicker', (data = new Timepicker(this, options)));
|
||||
}
|
||||
if (typeof option == 'string') {
|
||||
data[option]();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.timepicker.defaults = {
|
||||
minuteStep: 15
|
||||
, secondStep: 15
|
||||
, disableFocus: false
|
||||
, defaultTime: 'current'
|
||||
, showSeconds: false
|
||||
, showInputs: true
|
||||
, showMeridian: true
|
||||
, template: 'dropdown'
|
||||
, modalBackdrop: false
|
||||
, templates: {} // set custom templates
|
||||
}
|
||||
|
||||
$.fn.timepicker.Constructor = Timepicker
|
||||
}(window.jQuery);
|
||||
7
static/js/bootstrap.min.js
vendored
7
static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
2
static/js/jquery-3.7.1.min.js
vendored
2
static/js/jquery-3.7.1.min.js
vendored
File diff suppressed because one or more lines are too long
6
static/js/jquery-ui-1.10.1.custom.min.js
vendored
6
static/js/jquery-ui-1.10.1.custom.min.js
vendored
File diff suppressed because one or more lines are too long
1164
static/js/jquery.fileupload.js
vendored
1164
static/js/jquery.fileupload.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,185 +0,0 @@
|
||||
/*
|
||||
* jQuery Iframe Transport Plugin 1.6.1
|
||||
* https://github.com/blueimp/jQuery-File-Upload
|
||||
*
|
||||
* Copyright 2011, Sebastian Tschan
|
||||
* https://blueimp.net
|
||||
*
|
||||
* Licensed under the MIT license:
|
||||
* http://www.opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
/*jslint unparam: true, nomen: true */
|
||||
/*global define, window, document */
|
||||
|
||||
(function (factory) {
|
||||
'use strict';
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// Register as an anonymous AMD module:
|
||||
define(['jquery'], factory);
|
||||
} else {
|
||||
// Browser globals:
|
||||
factory(window.jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
'use strict';
|
||||
|
||||
// Helper variable to create unique names for the transport iframes:
|
||||
var counter = 0;
|
||||
|
||||
// The iframe transport accepts three additional options:
|
||||
// options.fileInput: a jQuery collection of file input fields
|
||||
// options.paramName: the parameter name for the file form data,
|
||||
// overrides the name property of the file input field(s),
|
||||
// can be a string or an array of strings.
|
||||
// options.formData: an array of objects with name and value properties,
|
||||
// equivalent to the return data of .serializeArray(), e.g.:
|
||||
// [{name: 'a', value: 1}, {name: 'b', value: 2}]
|
||||
$.ajaxTransport('iframe', function (options) {
|
||||
if (options.async) {
|
||||
var form,
|
||||
iframe,
|
||||
addParamChar;
|
||||
return {
|
||||
send: function (_, completeCallback) {
|
||||
form = $('<form style="display:none;"></form>');
|
||||
form.attr('accept-charset', options.formAcceptCharset);
|
||||
addParamChar = /\?/.test(options.url) ? '&' : '?';
|
||||
// XDomainRequest only supports GET and POST:
|
||||
if (options.type === 'DELETE') {
|
||||
options.url = options.url + addParamChar + '_method=DELETE';
|
||||
options.type = 'POST';
|
||||
} else if (options.type === 'PUT') {
|
||||
options.url = options.url + addParamChar + '_method=PUT';
|
||||
options.type = 'POST';
|
||||
} else if (options.type === 'PATCH') {
|
||||
options.url = options.url + addParamChar + '_method=PATCH';
|
||||
options.type = 'POST';
|
||||
}
|
||||
// javascript:false as initial iframe src
|
||||
// prevents warning popups on HTTPS in IE6.
|
||||
// IE versions below IE8 cannot set the name property of
|
||||
// elements that have already been added to the DOM,
|
||||
// so we set the name along with the iframe HTML markup:
|
||||
iframe = $(
|
||||
'<iframe src="javascript:false;" name="iframe-transport-' +
|
||||
(counter += 1) + '"></iframe>'
|
||||
).bind('load', function () {
|
||||
var fileInputClones,
|
||||
paramNames = $.isArray(options.paramName) ?
|
||||
options.paramName : [options.paramName];
|
||||
iframe
|
||||
.unbind('load')
|
||||
.bind('load', function () {
|
||||
var response;
|
||||
// Wrap in a try/catch block to catch exceptions thrown
|
||||
// when trying to access cross-domain iframe contents:
|
||||
try {
|
||||
response = iframe.contents();
|
||||
// Google Chrome and Firefox do not throw an
|
||||
// exception when calling iframe.contents() on
|
||||
// cross-domain requests, so we unify the response:
|
||||
if (!response.length || !response[0].firstChild) {
|
||||
throw new Error();
|
||||
}
|
||||
} catch (e) {
|
||||
response = undefined;
|
||||
}
|
||||
// The complete callback returns the
|
||||
// iframe content document as response object:
|
||||
completeCallback(
|
||||
200,
|
||||
'success',
|
||||
{'iframe': response}
|
||||
);
|
||||
// Fix for IE endless progress bar activity bug
|
||||
// (happens on form submits to iframe targets):
|
||||
$('<iframe src="javascript:false;"></iframe>')
|
||||
.appendTo(form);
|
||||
form.remove();
|
||||
});
|
||||
form
|
||||
.prop('target', iframe.prop('name'))
|
||||
.prop('action', options.url)
|
||||
.prop('method', options.type);
|
||||
if (options.formData) {
|
||||
$.each(options.formData, function (index, field) {
|
||||
$('<input type="hidden"/>')
|
||||
.prop('name', field.name)
|
||||
.val(field.value)
|
||||
.appendTo(form);
|
||||
});
|
||||
}
|
||||
if (options.fileInput && options.fileInput.length &&
|
||||
options.type === 'POST') {
|
||||
fileInputClones = options.fileInput.clone();
|
||||
// Insert a clone for each file input field:
|
||||
options.fileInput.after(function (index) {
|
||||
return fileInputClones[index];
|
||||
});
|
||||
if (options.paramName) {
|
||||
options.fileInput.each(function (index) {
|
||||
$(this).prop(
|
||||
'name',
|
||||
paramNames[index] || options.paramName
|
||||
);
|
||||
});
|
||||
}
|
||||
// Appending the file input fields to the hidden form
|
||||
// removes them from their original location:
|
||||
form
|
||||
.append(options.fileInput)
|
||||
.prop('enctype', 'multipart/form-data')
|
||||
// enctype must be set as encoding for IE:
|
||||
.prop('encoding', 'multipart/form-data');
|
||||
}
|
||||
form.submit();
|
||||
// Insert the file input fields at their original location
|
||||
// by replacing the clones with the originals:
|
||||
if (fileInputClones && fileInputClones.length) {
|
||||
options.fileInput.each(function (index, input) {
|
||||
var clone = $(fileInputClones[index]);
|
||||
$(input).prop('name', clone.prop('name'));
|
||||
clone.replaceWith(input);
|
||||
});
|
||||
}
|
||||
});
|
||||
form.append(iframe).appendTo(document.body);
|
||||
},
|
||||
abort: function () {
|
||||
if (iframe) {
|
||||
// javascript:false as iframe src aborts the request
|
||||
// and prevents warning popups on HTTPS in IE6.
|
||||
// concat is used to avoid the "Script URL" JSLint error:
|
||||
iframe
|
||||
.unbind('load')
|
||||
.prop('src', 'javascript'.concat(':false;'));
|
||||
}
|
||||
if (form) {
|
||||
form.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// The iframe transport returns the iframe content document as response.
|
||||
// The following adds converters from iframe to text, json, html, and script:
|
||||
$.ajaxSetup({
|
||||
converters: {
|
||||
'iframe text': function (iframe) {
|
||||
return iframe && $(iframe[0].body).text();
|
||||
},
|
||||
'iframe json': function (iframe) {
|
||||
return iframe && $.parseJSON($(iframe[0].body).text());
|
||||
},
|
||||
'iframe html': function (iframe) {
|
||||
return iframe && $(iframe[0].body).html();
|
||||
},
|
||||
'iframe script': function (iframe) {
|
||||
return iframe && $.globalEval($(iframe[0].body).text());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}));
|
||||
@@ -1,5 +0,0 @@
|
||||
jQuery(function() {
|
||||
Anthias.app = new Anthias.App({
|
||||
el: $('body')
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
5
static/js/popper.min.js
vendored
5
static/js/popper.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,120 +0,0 @@
|
||||
import '../sass/anthias.scss'
|
||||
|
||||
$().ready ->
|
||||
|
||||
$("#request-error .close").click (e) ->
|
||||
$("#request-error .alert").hide()
|
||||
|
||||
$("#btn-backup").click (e) ->
|
||||
btnText = $("#btn-backup").text()
|
||||
$("#btn-backup").text "Preparing archive..."
|
||||
|
||||
$("#btn-upload").prop "disabled", yes
|
||||
$("#btn-backup").prop "disabled", yes
|
||||
|
||||
$.ajax({
|
||||
method: "POST"
|
||||
url: "/api/v2/backup"
|
||||
timeout: 1800 * 1000
|
||||
})
|
||||
|
||||
.done (data, e) ->
|
||||
if (data)
|
||||
window.location = "/static_with_mime/" + data + "?mime=application/x-tgz"
|
||||
|
||||
.fail (data, e) ->
|
||||
$("#request-error .alert").addClass "alert-danger"
|
||||
$("#request-error .alert").removeClass "alert-success"
|
||||
$("#request-error .alert").show()
|
||||
if (data.responseText != "") and (j = $.parseJSON data.responseText) and (err = j.error)
|
||||
($ "#request-error .msg").text "Server Error: " + err
|
||||
else
|
||||
($ "#request-error .msg").text "The operation failed. Please reload the page and try again."
|
||||
|
||||
.always (data, e) ->
|
||||
$("#btn-backup").text btnText
|
||||
$("#btn-upload").prop "disabled", no
|
||||
$("#btn-backup").prop "disabled", no
|
||||
|
||||
|
||||
$("#btn-upload").click (e) ->
|
||||
e.preventDefault()
|
||||
$("[name='backup_upload']").click()
|
||||
|
||||
$("[name='backup_upload']").fileupload
|
||||
url: "/api/v2/recover"
|
||||
progressall: (e, data) -> if data.loaded and data.total
|
||||
valuenow = data.loaded/data.total*100
|
||||
$(".progress .bar").css "width", valuenow + "%"
|
||||
$(".progress .bar").text "Uploading: " + Math.floor(valuenow) + "%"
|
||||
add: (e, data) ->
|
||||
$("#btn-upload").hide()
|
||||
$("#btn-backup").hide()
|
||||
$(".progress").show()
|
||||
|
||||
data.submit()
|
||||
done: (e, data) ->
|
||||
if (data.jqXHR.responseText != "") and (message = $.parseJSON data.jqXHR.responseText)
|
||||
$("#request-error .alert").show()
|
||||
$("#request-error .alert").addClass "alert-success"
|
||||
$("#request-error .alert").removeClass "alert-danger"
|
||||
($ "#request-error .msg").text message
|
||||
fail: (e, data) ->
|
||||
$("#request-error .alert").show()
|
||||
$("#request-error .alert").addClass "alert-danger"
|
||||
$("#request-error .alert").removeClass "alert-success"
|
||||
if (data.jqXHR.responseText != "") and (j = $.parseJSON data.jqXHR.responseText) and (err = j.error)
|
||||
($ "#request-error .msg").text "Server Error: " + err
|
||||
else
|
||||
($ "#request-error .msg").text "The operation failed. Please reload the page and try again."
|
||||
always: (e, data) ->
|
||||
$(".progress").hide()
|
||||
$("#btn-upload").show()
|
||||
$("#btn-backup").show()
|
||||
|
||||
$("#btn-reboot-system").click (e) ->
|
||||
if confirm "Are you sure you want to reboot your device?"
|
||||
$.post "/api/v2/reboot"
|
||||
.done (e) ->
|
||||
($ "#request-error .alert").show()
|
||||
($ "#request-error .alert").addClass "alert-success"
|
||||
($ "#request-error .alert").removeClass "alert-danger"
|
||||
($ "#request-error .msg").text "Reboot has started successfully."
|
||||
.fail (data, e) ->
|
||||
($ "#request-error .alert").show()
|
||||
($ "#request-error .alert").addClass "alert-danger"
|
||||
($ "#request-error .alert").removeClass "alert-success"
|
||||
if (data.responseText != "") and (j = $.parseJSON data.responseText) and (err = j.error)
|
||||
($ "#request-error .msg").text "Server Error: " + err
|
||||
else
|
||||
($ "#request-error .msg").text "The operation failed. Please reload the page and try again."
|
||||
|
||||
$("#btn-shutdown-system").click (e) ->
|
||||
if confirm "Are you sure you want to shutdown your device?"
|
||||
$.post "/api/v2/shutdown"
|
||||
.done (e) ->
|
||||
($ "#request-error .alert").show()
|
||||
($ "#request-error .alert").addClass "alert-success"
|
||||
($ "#request-error .alert").removeClass "alert-danger"
|
||||
($ "#request-error .msg").text """
|
||||
Device shutdown has started successfully.
|
||||
Soon you will be able to unplug the power from your Raspberry Pi.
|
||||
"""
|
||||
.fail (data, e) ->
|
||||
($ "#request-error .alert").show()
|
||||
($ "#request-error .alert").addClass "alert-danger"
|
||||
($ "#request-error .alert").removeClass "alert-success"
|
||||
if (data.responseText != "") and (j = $.parseJSON data.responseText) and (err = j.error)
|
||||
($ "#request-error .msg").text "Server Error: " + err
|
||||
else
|
||||
($ "#request-error .msg").text "The operation failed. Please reload the page and try again."
|
||||
|
||||
toggle_chunk = () ->
|
||||
$("[id^=auth_chunk]").hide()
|
||||
$.each $('#auth_backend option'), (e, t) ->
|
||||
$('#auth_backend-'+t.value).toggle $('#auth_backend').val() == t.value
|
||||
|
||||
$('#auth_backend').change (e) ->
|
||||
toggle_chunk()
|
||||
|
||||
toggle_chunk()
|
||||
1
static/js/underscore-1.4.3.min.js
vendored
1
static/js/underscore-1.4.3.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -504,11 +504,11 @@ input[name="file_upload"] {
|
||||
}
|
||||
|
||||
.asset_row_btns {
|
||||
padding: 6px 8px 10px 8px !important;
|
||||
padding: 12px 8px 10px 8px !important;
|
||||
white-space: nowrap;
|
||||
|
||||
button {
|
||||
padding: 3px 10px;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,3 +555,23 @@ label.toggle {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 1em;
|
||||
background-color: #dedede;
|
||||
border-radius: 4px;
|
||||
animation: placeholderGlow 0.85s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes placeholderGlow {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
filter: brightness(1);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,681 +0,0 @@
|
||||
jasmine.HtmlReporterHelpers = {};
|
||||
|
||||
jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) {
|
||||
var el = document.createElement(type);
|
||||
|
||||
for (var i = 2; i < arguments.length; i++) {
|
||||
var child = arguments[i];
|
||||
|
||||
if (typeof child === 'string') {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else {
|
||||
if (child) {
|
||||
el.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var attr in attrs) {
|
||||
if (attr == "className") {
|
||||
el[attr] = attrs[attr];
|
||||
} else {
|
||||
el.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.getSpecStatus = function(child) {
|
||||
var results = child.results();
|
||||
var status = results.passed() ? 'passed' : 'failed';
|
||||
if (results.skipped) {
|
||||
status = 'skipped';
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) {
|
||||
var parentDiv = this.dom.summary;
|
||||
var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite';
|
||||
var parent = child[parentSuite];
|
||||
|
||||
if (parent) {
|
||||
if (typeof this.views.suites[parent.id] == 'undefined') {
|
||||
this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views);
|
||||
}
|
||||
parentDiv = this.views.suites[parent.id].element;
|
||||
}
|
||||
|
||||
parentDiv.appendChild(childElement);
|
||||
};
|
||||
|
||||
|
||||
jasmine.HtmlReporterHelpers.addHelpers = function(ctor) {
|
||||
for(var fn in jasmine.HtmlReporterHelpers) {
|
||||
ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn];
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter = function(_doc) {
|
||||
var self = this;
|
||||
var doc = _doc || window.document;
|
||||
|
||||
var reporterView;
|
||||
|
||||
var dom = {};
|
||||
|
||||
// Jasmine Reporter Public Interface
|
||||
self.logRunningSpecs = false;
|
||||
|
||||
self.reportRunnerStarting = function(runner) {
|
||||
var specs = runner.specs() || [];
|
||||
|
||||
if (specs.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
createReporterDom(runner.env.versionString());
|
||||
doc.body.appendChild(dom.reporter);
|
||||
setExceptionHandling();
|
||||
|
||||
reporterView = new jasmine.HtmlReporter.ReporterView(dom);
|
||||
reporterView.addSpecs(specs, self.specFilter);
|
||||
};
|
||||
|
||||
self.reportRunnerResults = function(runner) {
|
||||
reporterView && reporterView.complete();
|
||||
};
|
||||
|
||||
self.reportSuiteResults = function(suite) {
|
||||
reporterView.suiteComplete(suite);
|
||||
};
|
||||
|
||||
self.reportSpecStarting = function(spec) {
|
||||
if (self.logRunningSpecs) {
|
||||
self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
|
||||
}
|
||||
};
|
||||
|
||||
self.reportSpecResults = function(spec) {
|
||||
reporterView.specComplete(spec);
|
||||
};
|
||||
|
||||
self.log = function() {
|
||||
var console = jasmine.getGlobal().console;
|
||||
if (console && console.log) {
|
||||
if (console.log.apply) {
|
||||
console.log.apply(console, arguments);
|
||||
} else {
|
||||
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.specFilter = function(spec) {
|
||||
if (!focusedSpecName()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return spec.getFullName().indexOf(focusedSpecName()) === 0;
|
||||
};
|
||||
|
||||
return self;
|
||||
|
||||
function focusedSpecName() {
|
||||
var specName;
|
||||
|
||||
(function memoizeFocusedSpec() {
|
||||
if (specName) {
|
||||
return;
|
||||
}
|
||||
|
||||
var paramMap = [];
|
||||
var params = jasmine.HtmlReporter.parameters(doc);
|
||||
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var p = params[i].split('=');
|
||||
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
|
||||
}
|
||||
|
||||
specName = paramMap.spec;
|
||||
})();
|
||||
|
||||
return specName;
|
||||
}
|
||||
|
||||
function createReporterDom(version) {
|
||||
dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' },
|
||||
dom.banner = self.createDom('div', { className: 'banner' },
|
||||
self.createDom('span', { className: 'title' }, "Jasmine "),
|
||||
self.createDom('span', { className: 'version' }, version)),
|
||||
|
||||
dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}),
|
||||
dom.alert = self.createDom('div', {className: 'alert'},
|
||||
self.createDom('span', { className: 'exceptions' },
|
||||
self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'),
|
||||
self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))),
|
||||
dom.results = self.createDom('div', {className: 'results'},
|
||||
dom.summary = self.createDom('div', { className: 'summary' }),
|
||||
dom.details = self.createDom('div', { id: 'details' }))
|
||||
);
|
||||
}
|
||||
|
||||
function noTryCatch() {
|
||||
return window.location.search.match(/catch=false/);
|
||||
}
|
||||
|
||||
function searchWithCatch() {
|
||||
var params = jasmine.HtmlReporter.parameters(window.document);
|
||||
var removed = false;
|
||||
var i = 0;
|
||||
|
||||
while (!removed && i < params.length) {
|
||||
if (params[i].match(/catch=/)) {
|
||||
params.splice(i, 1);
|
||||
removed = true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (jasmine.CATCH_EXCEPTIONS) {
|
||||
params.push("catch=false");
|
||||
}
|
||||
|
||||
return params.join("&");
|
||||
}
|
||||
|
||||
function setExceptionHandling() {
|
||||
var chxCatch = document.getElementById('no_try_catch');
|
||||
|
||||
if (noTryCatch()) {
|
||||
chxCatch.setAttribute('checked', true);
|
||||
jasmine.CATCH_EXCEPTIONS = false;
|
||||
}
|
||||
chxCatch.onclick = function() {
|
||||
window.location.search = searchWithCatch();
|
||||
};
|
||||
}
|
||||
};
|
||||
jasmine.HtmlReporter.parameters = function(doc) {
|
||||
var paramStr = doc.location.search.substring(1);
|
||||
var params = [];
|
||||
|
||||
if (paramStr.length > 0) {
|
||||
params = paramStr.split('&');
|
||||
}
|
||||
return params;
|
||||
}
|
||||
jasmine.HtmlReporter.sectionLink = function(sectionName) {
|
||||
var link = '?';
|
||||
var params = [];
|
||||
|
||||
if (sectionName) {
|
||||
params.push('spec=' + encodeURIComponent(sectionName));
|
||||
}
|
||||
if (!jasmine.CATCH_EXCEPTIONS) {
|
||||
params.push("catch=false");
|
||||
}
|
||||
if (params.length > 0) {
|
||||
link += params.join("&");
|
||||
}
|
||||
|
||||
return link;
|
||||
};
|
||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);
|
||||
jasmine.HtmlReporter.ReporterView = function(dom) {
|
||||
this.startedAt = new Date();
|
||||
this.runningSpecCount = 0;
|
||||
this.completeSpecCount = 0;
|
||||
this.passedCount = 0;
|
||||
this.failedCount = 0;
|
||||
this.skippedCount = 0;
|
||||
|
||||
this.createResultsMenu = function() {
|
||||
this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'},
|
||||
this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'),
|
||||
' | ',
|
||||
this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing'));
|
||||
|
||||
this.summaryMenuItem.onclick = function() {
|
||||
dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, '');
|
||||
};
|
||||
|
||||
this.detailsMenuItem.onclick = function() {
|
||||
showDetails();
|
||||
};
|
||||
};
|
||||
|
||||
this.addSpecs = function(specs, specFilter) {
|
||||
this.totalSpecCount = specs.length;
|
||||
|
||||
this.views = {
|
||||
specs: {},
|
||||
suites: {}
|
||||
};
|
||||
|
||||
for (var i = 0; i < specs.length; i++) {
|
||||
var spec = specs[i];
|
||||
this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views);
|
||||
if (specFilter(spec)) {
|
||||
this.runningSpecCount++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.specComplete = function(spec) {
|
||||
this.completeSpecCount++;
|
||||
|
||||
if (isUndefined(this.views.specs[spec.id])) {
|
||||
this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom);
|
||||
}
|
||||
|
||||
var specView = this.views.specs[spec.id];
|
||||
|
||||
switch (specView.status()) {
|
||||
case 'passed':
|
||||
this.passedCount++;
|
||||
break;
|
||||
|
||||
case 'failed':
|
||||
this.failedCount++;
|
||||
break;
|
||||
|
||||
case 'skipped':
|
||||
this.skippedCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
specView.refresh();
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
this.suiteComplete = function(suite) {
|
||||
var suiteView = this.views.suites[suite.id];
|
||||
if (isUndefined(suiteView)) {
|
||||
return;
|
||||
}
|
||||
suiteView.refresh();
|
||||
};
|
||||
|
||||
this.refresh = function() {
|
||||
|
||||
if (isUndefined(this.resultsMenu)) {
|
||||
this.createResultsMenu();
|
||||
}
|
||||
|
||||
// currently running UI
|
||||
if (isUndefined(this.runningAlert)) {
|
||||
this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" });
|
||||
dom.alert.appendChild(this.runningAlert);
|
||||
}
|
||||
this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount);
|
||||
|
||||
// skipped specs UI
|
||||
if (isUndefined(this.skippedAlert)) {
|
||||
this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" });
|
||||
}
|
||||
|
||||
this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
|
||||
|
||||
if (this.skippedCount === 1 && isDefined(dom.alert)) {
|
||||
dom.alert.appendChild(this.skippedAlert);
|
||||
}
|
||||
|
||||
// passing specs UI
|
||||
if (isUndefined(this.passedAlert)) {
|
||||
this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" });
|
||||
}
|
||||
this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount);
|
||||
|
||||
// failing specs UI
|
||||
if (isUndefined(this.failedAlert)) {
|
||||
this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"});
|
||||
}
|
||||
this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount);
|
||||
|
||||
if (this.failedCount === 1 && isDefined(dom.alert)) {
|
||||
dom.alert.appendChild(this.failedAlert);
|
||||
dom.alert.appendChild(this.resultsMenu);
|
||||
}
|
||||
|
||||
// summary info
|
||||
this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount);
|
||||
this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing";
|
||||
};
|
||||
|
||||
this.complete = function() {
|
||||
dom.alert.removeChild(this.runningAlert);
|
||||
|
||||
this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
|
||||
|
||||
if (this.failedCount === 0) {
|
||||
dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount)));
|
||||
} else {
|
||||
showDetails();
|
||||
}
|
||||
|
||||
dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"));
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
function showDetails() {
|
||||
if (dom.reporter.className.search(/showDetails/) === -1) {
|
||||
dom.reporter.className += " showDetails";
|
||||
}
|
||||
}
|
||||
|
||||
function isUndefined(obj) {
|
||||
return typeof obj === 'undefined';
|
||||
}
|
||||
|
||||
function isDefined(obj) {
|
||||
return !isUndefined(obj);
|
||||
}
|
||||
|
||||
function specPluralizedFor(count) {
|
||||
var str = count + " spec";
|
||||
if (count > 1) {
|
||||
str += "s"
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView);
|
||||
|
||||
|
||||
jasmine.HtmlReporter.SpecView = function(spec, dom, views) {
|
||||
this.spec = spec;
|
||||
this.dom = dom;
|
||||
this.views = views;
|
||||
|
||||
this.symbol = this.createDom('li', { className: 'pending' });
|
||||
this.dom.symbolSummary.appendChild(this.symbol);
|
||||
|
||||
this.summary = this.createDom('div', { className: 'specSummary' },
|
||||
this.createDom('a', {
|
||||
className: 'description',
|
||||
href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()),
|
||||
title: this.spec.getFullName()
|
||||
}, this.spec.description)
|
||||
);
|
||||
|
||||
this.detail = this.createDom('div', { className: 'specDetail' },
|
||||
this.createDom('a', {
|
||||
className: 'description',
|
||||
href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
|
||||
title: this.spec.getFullName()
|
||||
}, this.spec.getFullName())
|
||||
);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SpecView.prototype.status = function() {
|
||||
return this.getSpecStatus(this.spec);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SpecView.prototype.refresh = function() {
|
||||
this.symbol.className = this.status();
|
||||
|
||||
switch (this.status()) {
|
||||
case 'skipped':
|
||||
break;
|
||||
|
||||
case 'passed':
|
||||
this.appendSummaryToSuiteDiv();
|
||||
break;
|
||||
|
||||
case 'failed':
|
||||
this.appendSummaryToSuiteDiv();
|
||||
this.appendFailureDetail();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() {
|
||||
this.summary.className += ' ' + this.status();
|
||||
this.appendToSummary(this.spec, this.summary);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() {
|
||||
this.detail.className += ' ' + this.status();
|
||||
|
||||
var resultItems = this.spec.results().getItems();
|
||||
var messagesDiv = this.createDom('div', { className: 'messages' });
|
||||
|
||||
for (var i = 0; i < resultItems.length; i++) {
|
||||
var result = resultItems[i];
|
||||
|
||||
if (result.type == 'log') {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
|
||||
} else if (result.type == 'expect' && result.passed && !result.passed()) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
|
||||
|
||||
if (result.trace.stack) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messagesDiv.childNodes.length > 0) {
|
||||
this.detail.appendChild(messagesDiv);
|
||||
this.dom.details.appendChild(this.detail);
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) {
|
||||
this.suite = suite;
|
||||
this.dom = dom;
|
||||
this.views = views;
|
||||
|
||||
this.element = this.createDom('div', { className: 'suite' },
|
||||
this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description)
|
||||
);
|
||||
|
||||
this.appendToSummary(this.suite, this.element);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SuiteView.prototype.status = function() {
|
||||
return this.getSpecStatus(this.suite);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SuiteView.prototype.refresh = function() {
|
||||
this.element.className += " " + this.status();
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView);
|
||||
|
||||
/* @deprecated Use jasmine.HtmlReporter instead
|
||||
*/
|
||||
jasmine.TrivialReporter = function(doc) {
|
||||
this.document = doc || document;
|
||||
this.suiteDivs = {};
|
||||
this.logRunningSpecs = false;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
|
||||
var el = document.createElement(type);
|
||||
|
||||
for (var i = 2; i < arguments.length; i++) {
|
||||
var child = arguments[i];
|
||||
|
||||
if (typeof child === 'string') {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else {
|
||||
if (child) { el.appendChild(child); }
|
||||
}
|
||||
}
|
||||
|
||||
for (var attr in attrs) {
|
||||
if (attr == "className") {
|
||||
el[attr] = attrs[attr];
|
||||
} else {
|
||||
el.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
|
||||
var showPassed, showSkipped;
|
||||
|
||||
this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' },
|
||||
this.createDom('div', { className: 'banner' },
|
||||
this.createDom('div', { className: 'logo' },
|
||||
this.createDom('span', { className: 'title' }, "Jasmine"),
|
||||
this.createDom('span', { className: 'version' }, runner.env.versionString())),
|
||||
this.createDom('div', { className: 'options' },
|
||||
"Show ",
|
||||
showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
|
||||
this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
|
||||
showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
|
||||
this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
|
||||
)
|
||||
),
|
||||
|
||||
this.runnerDiv = this.createDom('div', { className: 'runner running' },
|
||||
this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
|
||||
this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
|
||||
this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
|
||||
);
|
||||
|
||||
this.document.body.appendChild(this.outerDiv);
|
||||
|
||||
var suites = runner.suites();
|
||||
for (var i = 0; i < suites.length; i++) {
|
||||
var suite = suites[i];
|
||||
var suiteDiv = this.createDom('div', { className: 'suite' },
|
||||
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
|
||||
this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
|
||||
this.suiteDivs[suite.id] = suiteDiv;
|
||||
var parentDiv = this.outerDiv;
|
||||
if (suite.parentSuite) {
|
||||
parentDiv = this.suiteDivs[suite.parentSuite.id];
|
||||
}
|
||||
parentDiv.appendChild(suiteDiv);
|
||||
}
|
||||
|
||||
this.startedAt = new Date();
|
||||
|
||||
var self = this;
|
||||
showPassed.onclick = function(evt) {
|
||||
if (showPassed.checked) {
|
||||
self.outerDiv.className += ' show-passed';
|
||||
} else {
|
||||
self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
|
||||
}
|
||||
};
|
||||
|
||||
showSkipped.onclick = function(evt) {
|
||||
if (showSkipped.checked) {
|
||||
self.outerDiv.className += ' show-skipped';
|
||||
} else {
|
||||
self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
|
||||
var results = runner.results();
|
||||
var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
|
||||
this.runnerDiv.setAttribute("class", className);
|
||||
//do it twice for IE
|
||||
this.runnerDiv.setAttribute("className", className);
|
||||
var specs = runner.specs();
|
||||
var specCount = 0;
|
||||
for (var i = 0; i < specs.length; i++) {
|
||||
if (this.specFilter(specs[i])) {
|
||||
specCount++;
|
||||
}
|
||||
}
|
||||
var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
|
||||
message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
|
||||
this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
|
||||
|
||||
this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
|
||||
var results = suite.results();
|
||||
var status = results.passed() ? 'passed' : 'failed';
|
||||
if (results.totalCount === 0) { // todo: change this to check results.skipped
|
||||
status = 'skipped';
|
||||
}
|
||||
this.suiteDivs[suite.id].className += " " + status;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
|
||||
if (this.logRunningSpecs) {
|
||||
this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
|
||||
var results = spec.results();
|
||||
var status = results.passed() ? 'passed' : 'failed';
|
||||
if (results.skipped) {
|
||||
status = 'skipped';
|
||||
}
|
||||
var specDiv = this.createDom('div', { className: 'spec ' + status },
|
||||
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
|
||||
this.createDom('a', {
|
||||
className: 'description',
|
||||
href: '?spec=' + encodeURIComponent(spec.getFullName()),
|
||||
title: spec.getFullName()
|
||||
}, spec.description));
|
||||
|
||||
|
||||
var resultItems = results.getItems();
|
||||
var messagesDiv = this.createDom('div', { className: 'messages' });
|
||||
for (var i = 0; i < resultItems.length; i++) {
|
||||
var result = resultItems[i];
|
||||
|
||||
if (result.type == 'log') {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
|
||||
} else if (result.type == 'expect' && result.passed && !result.passed()) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
|
||||
|
||||
if (result.trace.stack) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messagesDiv.childNodes.length > 0) {
|
||||
specDiv.appendChild(messagesDiv);
|
||||
}
|
||||
|
||||
this.suiteDivs[spec.suite.id].appendChild(specDiv);
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.log = function() {
|
||||
var console = jasmine.getGlobal().console;
|
||||
if (console && console.log) {
|
||||
if (console.log.apply) {
|
||||
console.log.apply(console, arguments);
|
||||
} else {
|
||||
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.getLocation = function() {
|
||||
return this.document.location;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.specFilter = function(spec) {
|
||||
var paramMap = {};
|
||||
var params = this.getLocation().search.substring(1).split('&');
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var p = params[i].split('=');
|
||||
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
|
||||
}
|
||||
|
||||
if (!paramMap.spec) {
|
||||
return true;
|
||||
}
|
||||
return spec.getFullName().indexOf(paramMap.spec) === 0;
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
|
||||
|
||||
#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
|
||||
#HTMLReporter a { text-decoration: none; }
|
||||
#HTMLReporter a:hover { text-decoration: underline; }
|
||||
#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; }
|
||||
#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; }
|
||||
#HTMLReporter #jasmine_content { position: fixed; right: 100%; }
|
||||
#HTMLReporter .version { color: #aaaaaa; }
|
||||
#HTMLReporter .banner { margin-top: 14px; }
|
||||
#HTMLReporter .duration { color: #aaaaaa; float: right; }
|
||||
#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; }
|
||||
#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; }
|
||||
#HTMLReporter .symbolSummary li.passed { font-size: 14px; }
|
||||
#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; }
|
||||
#HTMLReporter .symbolSummary li.failed { line-height: 9px; }
|
||||
#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
|
||||
#HTMLReporter .symbolSummary li.skipped { font-size: 14px; }
|
||||
#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; }
|
||||
#HTMLReporter .symbolSummary li.pending { line-height: 11px; }
|
||||
#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; }
|
||||
#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
|
||||
#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
|
||||
#HTMLReporter .runningAlert { background-color: #666666; }
|
||||
#HTMLReporter .skippedAlert { background-color: #aaaaaa; }
|
||||
#HTMLReporter .skippedAlert:first-child { background-color: #333333; }
|
||||
#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; }
|
||||
#HTMLReporter .passingAlert { background-color: #a6b779; }
|
||||
#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; }
|
||||
#HTMLReporter .failingAlert { background-color: #cf867e; }
|
||||
#HTMLReporter .failingAlert:first-child { background-color: #b03911; }
|
||||
#HTMLReporter .results { margin-top: 14px; }
|
||||
#HTMLReporter #details { display: none; }
|
||||
#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; }
|
||||
#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
|
||||
#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
|
||||
#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
|
||||
#HTMLReporter.showDetails .summary { display: none; }
|
||||
#HTMLReporter.showDetails #details { display: block; }
|
||||
#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
|
||||
#HTMLReporter .summary { margin-top: 14px; }
|
||||
#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; }
|
||||
#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; }
|
||||
#HTMLReporter .summary .specSummary.failed a { color: #b03911; }
|
||||
#HTMLReporter .description + .suite { margin-top: 0; }
|
||||
#HTMLReporter .suite { margin-top: 14px; }
|
||||
#HTMLReporter .suite a { color: #333333; }
|
||||
#HTMLReporter #details .specDetail { margin-bottom: 28px; }
|
||||
#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; }
|
||||
#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; }
|
||||
#HTMLReporter .resultMessage span.result { display: block; }
|
||||
#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
|
||||
|
||||
#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ }
|
||||
#TrivialReporter a:visited, #TrivialReporter a { color: #303; }
|
||||
#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; }
|
||||
#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; }
|
||||
#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; }
|
||||
#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; }
|
||||
#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; }
|
||||
#TrivialReporter .runner.running { background-color: yellow; }
|
||||
#TrivialReporter .options { text-align: right; font-size: .8em; }
|
||||
#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; }
|
||||
#TrivialReporter .suite .suite { margin: 5px; }
|
||||
#TrivialReporter .suite.passed { background-color: #dfd; }
|
||||
#TrivialReporter .suite.failed { background-color: #fdd; }
|
||||
#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; }
|
||||
#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; }
|
||||
#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; }
|
||||
#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; }
|
||||
#TrivialReporter .spec.skipped { background-color: #bbb; }
|
||||
#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; }
|
||||
#TrivialReporter .passed { background-color: #cfc; display: none; }
|
||||
#TrivialReporter .failed { background-color: #fbb; }
|
||||
#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; }
|
||||
#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; }
|
||||
#TrivialReporter .resultMessage .mismatch { color: black; }
|
||||
#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; }
|
||||
#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; }
|
||||
#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; }
|
||||
#TrivialReporter #jasmine_content { position: fixed; right: 100%; }
|
||||
#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,146 +0,0 @@
|
||||
(function() {
|
||||
if (! jasmine) {
|
||||
throw new Exception("jasmine library does not exist in global namespace!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic reporter that outputs spec results to the browser console.
|
||||
* Useful if you need to test an html page and don't want the TrivialReporter
|
||||
* markup mucking things up.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* jasmine.getEnv().addReporter(new jasmine.ConsoleReporter());
|
||||
* jasmine.getEnv().execute();
|
||||
*/
|
||||
var ConsoleReporter = function() {
|
||||
this.started = false;
|
||||
this.finished = false;
|
||||
};
|
||||
|
||||
ConsoleReporter.prototype = {
|
||||
reportRunnerResults: function(runner) {
|
||||
if (this.hasGroupedConsole()) {
|
||||
var suites = runner.suites();
|
||||
startGroup(runner.results(), 'tests');
|
||||
for (var i=0; i<suites.length; i++) {
|
||||
if (!suites[i].parentSuite) {
|
||||
suiteResults(suites[i]);
|
||||
}
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
else {
|
||||
var dur = (new Date()).getTime() - this.start_time;
|
||||
var failed = this.executed_specs - this.passed_specs;
|
||||
var spec_str = this.executed_specs + (this.executed_specs === 1 ? " spec, " : " specs, ");
|
||||
var fail_str = failed + (failed === 1 ? " failure in " : " failures in ");
|
||||
|
||||
this.log("Runner Finished.");
|
||||
this.log(spec_str + fail_str + (dur/1000) + "s.");
|
||||
}
|
||||
this.finished = true;
|
||||
|
||||
if(window.inPhantom)
|
||||
confirm(failed === 0 ? 0 : 1)
|
||||
},
|
||||
|
||||
hasGroupedConsole: function() {
|
||||
return false;
|
||||
},
|
||||
|
||||
reportRunnerStarting: function(runner) {
|
||||
this.started = true;
|
||||
if (!this.hasGroupedConsole()) {
|
||||
this.start_time = (new Date()).getTime();
|
||||
this.executed_specs = 0;
|
||||
this.passed_specs = 0;
|
||||
this.log("Runner Started.");
|
||||
}
|
||||
},
|
||||
|
||||
reportSpecResults: function(spec) {
|
||||
if (!this.hasGroupedConsole()) {
|
||||
var resultText = "Failed.";
|
||||
|
||||
if (spec.results().skipped ) {
|
||||
resultText = 'Skipped.';
|
||||
} else if (spec.results().passed()) {
|
||||
this.passed_specs++;
|
||||
resultText = "Passed.";
|
||||
}
|
||||
|
||||
this.log(resultText);
|
||||
}
|
||||
},
|
||||
|
||||
reportSpecStarting: function(spec) {
|
||||
if (!this.hasGroupedConsole()) {
|
||||
this.executed_specs++;
|
||||
this.log(spec.suite.description + ' : ' + spec.description + ' ... ');
|
||||
}
|
||||
},
|
||||
|
||||
reportSuiteResults: function(suite) {
|
||||
if (!this.hasGroupedConsole()) {
|
||||
var results = suite.results();
|
||||
this.log(suite.description + ": " + results.passedCount + " of " + results.totalCount + " passed.");
|
||||
}
|
||||
},
|
||||
|
||||
log: function(str) {
|
||||
var console = jasmine.getGlobal().console;
|
||||
if (console && console.log) {
|
||||
console.log(str);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function suiteResults(suite) {
|
||||
var results = suite.results();
|
||||
startGroup(results, suite.description);
|
||||
var specs = suite.specs();
|
||||
for (var i in specs) {
|
||||
if (specs.hasOwnProperty(i)) {
|
||||
specResults(specs[i]);
|
||||
}
|
||||
}
|
||||
var suites = suite.suites();
|
||||
for (var j in suites) {
|
||||
if (suites.hasOwnProperty(j)) {
|
||||
suiteResults(suites[j]);
|
||||
}
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
function specResults(spec) {
|
||||
var results = spec.results();
|
||||
startGroup(results, spec.description);
|
||||
var items = results.getItems();
|
||||
for (var k in items) {
|
||||
if (items.hasOwnProperty(k)) {
|
||||
itemResults(items[k]);
|
||||
}
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
function itemResults(item) {
|
||||
if (item.passed && !item.passed()) {
|
||||
console.warn({actual:item.actual,expected: item.expected});
|
||||
item.trace.message = item.matcherName;
|
||||
console.error(item.trace);
|
||||
} else {
|
||||
console.info('Passed');
|
||||
}
|
||||
}
|
||||
|
||||
function startGroup(results, description) {
|
||||
var consoleFunc = (results.passed() && console.groupCollapsed) ? 'groupCollapsed' : 'group';
|
||||
console[consoleFunc](description + ' (' + results.passedCount + '/' + results.totalCount + ' passed, ' + results.failedCount + ' failures)');
|
||||
}
|
||||
|
||||
// export public
|
||||
jasmine.ConsoleReporter = ConsoleReporter;
|
||||
})();
|
||||
@@ -1,24 +0,0 @@
|
||||
var webPage = require('webpage');
|
||||
var page = webPage.create();
|
||||
var url = 'http://localhost:8081/static/spec/runner.html';
|
||||
|
||||
page.onConsoleMessage = function(msg, lineNum, sourceId) {
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
page.onInitialized = function() {
|
||||
page.evaluate(function() {
|
||||
window.inPhantom = true;
|
||||
});
|
||||
};
|
||||
|
||||
page.open(url, function (status) {
|
||||
if(status !== 'success') {
|
||||
console.log('cannot load page ' + url);
|
||||
phantom.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
page.onConfirm = function(status) {
|
||||
phantom.exit(status);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Jasmine Spec Runner</title>
|
||||
<link rel="stylesheet" type="text/css" href="jasmine/jasmine.css">
|
||||
<script src="jasmine/jasmine.js"></script>
|
||||
<script src="jasmine/jasmine-html.js"></script>
|
||||
<script src="jasmine/phantom-console-reporter.js"></script>
|
||||
|
||||
<!-- include source files here... -->
|
||||
<script>
|
||||
var defaultDuration = 10;
|
||||
var use24HourClock = false;
|
||||
var dateFormat = "mm/dd/yyyy";
|
||||
</script>
|
||||
<script src="/static/js/underscore-1.4.3.min.js"></script>
|
||||
<script src="/static/js/jquery-1.9.1.min.js"></script>
|
||||
<script src="/static/js/jquery.iframe-transport.js"></script>
|
||||
<script src="/static/js/backbone-0.9.10.min.js"></script>
|
||||
<script src="/static/js/jquery-ui-1.10.1.custom.min.js"></script>
|
||||
<script src="/static/js/bootstrap.min.js"></script>
|
||||
<script src="/static/js/bootstrap-datepicker.js"></script>
|
||||
<script src="/static/js/bootstrap-timepicker.js"></script>
|
||||
<script src="/static/js/moment.js"></script>
|
||||
<script src="../dist/js/anthias.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var jasmineEnv = jasmine.getEnv();
|
||||
window.jenv = jasmineEnv;
|
||||
jasmineEnv.updateInterval = 2000;
|
||||
|
||||
var trivialReporter = new jasmine.TrivialReporter();
|
||||
|
||||
jasmineEnv.addReporter(trivialReporter);
|
||||
jasmineEnv.addReporter(new jasmine.ConsoleReporter());
|
||||
|
||||
jasmineEnv.specFilter = function(spec) {
|
||||
return trivialReporter.specFilter(spec);
|
||||
};
|
||||
|
||||
var currentWindowOnload = window.onload;
|
||||
|
||||
window.onload = function() {
|
||||
if (currentWindowOnload) {
|
||||
currentWindowOnload();
|
||||
}
|
||||
|
||||
jasmineEnv.execute();
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="screenly-spec.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
for i in {1..10}; do
|
||||
timeout 7 phantomjs static/spec/phantom-runner.js
|
||||
last=$?
|
||||
if [[ "$last" -ne "124" ]]; then
|
||||
exit $last
|
||||
fi
|
||||
echo "Timeout reach. Retrying."
|
||||
done
|
||||
|
||||
echo "Max retries reached."
|
||||
exit 1
|
||||
@@ -1,148 +0,0 @@
|
||||
// Generated by CoffeeScript 1.12.5
|
||||
(function() {
|
||||
describe("Anthias Open Source", function() {
|
||||
it("should have a Anthias object at its root", function() {
|
||||
return expect(Anthias).toBeDefined();
|
||||
});
|
||||
describe("date_to", function() {
|
||||
var a_date, test_date;
|
||||
test_date = new Date(2014, 5, 6, 14, 20, 0, 0);
|
||||
a_date = Anthias.date_to(test_date);
|
||||
it("should format date and time as 'MM/DD/YYYY hh:mm:ss A'", function() {
|
||||
return expect(a_date.string()).toBe('06/06/2014 02:20:00 PM');
|
||||
});
|
||||
it("should format date as 'MM/a_date/YYYY'", function() {
|
||||
return expect(a_date.date()).toBe('06/06/2014');
|
||||
});
|
||||
return it("should format date as 'hh:mm:ss A'", function() {
|
||||
return expect(a_date.time()).toBe('02:20 PM');
|
||||
});
|
||||
});
|
||||
describe("Models", function() {
|
||||
return describe("Asset model", function() {
|
||||
var asset, end_date, start_date;
|
||||
it("should exist", function() {
|
||||
return expect(Anthias.Asset).toBeDefined();
|
||||
});
|
||||
start_date = new Date(2014, 4, 6, 14, 20, 0, 0);
|
||||
end_date = new Date();
|
||||
end_date.setMonth(end_date.getMonth() + 2);
|
||||
asset = new Anthias.Asset({
|
||||
asset_id: 2,
|
||||
duration: "8",
|
||||
end_date: end_date,
|
||||
is_enabled: true,
|
||||
mimetype: 'webpage',
|
||||
name: 'Test',
|
||||
start_date: start_date,
|
||||
uri: 'https://anthias.screenly.io'
|
||||
});
|
||||
it("should be active if enabled and date is in range", function() {
|
||||
return expect(asset.active()).toBe(true);
|
||||
});
|
||||
it("should be inactive if disabled and date is in range", function() {
|
||||
asset.set('is_enabled', false);
|
||||
return expect(asset.active()).toBe(false);
|
||||
});
|
||||
it("should be inactive if enabled and date is out of range", function() {
|
||||
asset.set('is_enabled', true);
|
||||
asset.set('start_date', asset.get('end_date'));
|
||||
return expect(asset.active()).toBe(false);
|
||||
});
|
||||
it("should rollback to backup data if it exists", function() {
|
||||
asset.set('start_date', start_date);
|
||||
asset.set('end_date', end_date);
|
||||
asset.backup();
|
||||
asset.set({
|
||||
is_enabled: false,
|
||||
name: "Test 2",
|
||||
start_date: new Date(2011, 4, 6, 14, 20, 0, 0),
|
||||
end_date: new Date(2011, 4, 6, 14, 20, 0, 0),
|
||||
uri: "http://www.wireload.net"
|
||||
});
|
||||
asset.rollback();
|
||||
expect(asset.get('is_enabled')).toBe(true);
|
||||
expect(asset.get('name')).toBe('Test');
|
||||
expect(asset.get('start_date')).toBe(start_date);
|
||||
return expect(asset.get('uri')).toBe("https://anthias.screenly.io");
|
||||
});
|
||||
return it("should erase backup date after rollback", function() {
|
||||
asset.set({
|
||||
is_enabled: false,
|
||||
name: "Test 2",
|
||||
start_date: new Date(2011, 4, 6, 14, 20, 0, 0),
|
||||
end_date: new Date(2011, 4, 6, 14, 20, 0, 0),
|
||||
uri: "http://www.wireload.net"
|
||||
});
|
||||
asset.rollback();
|
||||
expect(asset.get('is_enabled')).toBe(false);
|
||||
expect(asset.get('name')).toBe('Test 2');
|
||||
expect(asset.get('start_date').toISOString()).toBe((new Date(2011, 4, 6, 14, 20, 0, 0)).toISOString());
|
||||
return expect(asset.get('uri')).toBe("http://www.wireload.net");
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Collections", function() {
|
||||
return describe("Assets", function() {
|
||||
it("should exist", function() {
|
||||
return expect(Anthias.Assets).toBeDefined();
|
||||
});
|
||||
it("should use the Asset model", function() {
|
||||
var assets;
|
||||
assets = new Anthias.Assets();
|
||||
return expect(assets.model).toBe(Anthias.Asset);
|
||||
});
|
||||
return it("should keep play order of assets", function() {
|
||||
var asset1, asset2, asset3, assets;
|
||||
assets = new Anthias.Assets();
|
||||
asset1 = new Anthias.Asset({
|
||||
asset_id: 1,
|
||||
is_enabled: true,
|
||||
name: 'AAA',
|
||||
uri: 'https://anthias.screenly.io',
|
||||
play_order: 2
|
||||
});
|
||||
asset2 = new Anthias.Asset({
|
||||
asset_id: 2,
|
||||
is_enabled: true,
|
||||
name: 'BBB',
|
||||
uri: 'https://anthias.screenly.io',
|
||||
play_order: 1
|
||||
});
|
||||
asset3 = new Anthias.Asset({
|
||||
asset_id: 3,
|
||||
is_enabled: true,
|
||||
name: 'CCC',
|
||||
uri: 'https://anthias.screenly.io',
|
||||
play_order: 0
|
||||
});
|
||||
assets.add([asset1, asset2, asset3]);
|
||||
expect(assets.at(0)).toBe(asset3);
|
||||
expect(assets.at(1)).toBe(asset2);
|
||||
expect(assets.at(2)).toBe(asset1);
|
||||
asset1.set('play_order', 0);
|
||||
asset3.set('play_order', 2);
|
||||
assets.sort();
|
||||
expect(assets.at(0)).toBe(asset1);
|
||||
expect(assets.at(1)).toBe(asset2);
|
||||
return expect(assets.at(2)).toBe(asset3);
|
||||
});
|
||||
});
|
||||
});
|
||||
return describe("Views", function() {
|
||||
it("should have AddAssetView", function() {
|
||||
return expect(Anthias.View.AddAssetView).toBeDefined();
|
||||
});
|
||||
it("should have EditAssetView", function() {
|
||||
return expect(Anthias.View.EditAssetView).toBeDefined();
|
||||
});
|
||||
it("should have AssetRowView", function() {
|
||||
return expect(Anthias.View.AssetRowView).toBeDefined();
|
||||
});
|
||||
return it("should have AssetsView", function() {
|
||||
return expect(Anthias.View.AssetsView).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
126
static/src/components/active-assets.jsx
Normal file
126
static/src/components/active-assets.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import {
|
||||
selectActiveAssets,
|
||||
updateAssetOrder,
|
||||
fetchAssets,
|
||||
} from '@/store/assets'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { SortableAssetRow } from '@/components/sortable-asset-row'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const ActiveAssetsTable = ({ onEditAsset }) => {
|
||||
const dispatch = useDispatch()
|
||||
const activeAssets = useSelector(selectActiveAssets)
|
||||
const [items, setItems] = useState(activeAssets)
|
||||
|
||||
useEffect(() => {
|
||||
setItems(activeAssets)
|
||||
}, [activeAssets])
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
|
||||
const handleDragEnd = async (event) => {
|
||||
const { active, over } = event
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return
|
||||
}
|
||||
|
||||
const oldIndex = items.findIndex(
|
||||
(asset) => asset.asset_id.toString() === active.id,
|
||||
)
|
||||
const newIndex = items.findIndex(
|
||||
(asset) => asset.asset_id.toString() === over.id,
|
||||
)
|
||||
|
||||
const newItems = arrayMove(items, oldIndex, newIndex)
|
||||
setItems(newItems)
|
||||
|
||||
const activeIds = newItems.map((asset) => asset.asset_id)
|
||||
|
||||
try {
|
||||
await dispatch(updateAssetOrder(activeIds.join(','))).unwrap()
|
||||
dispatch(fetchAssets())
|
||||
} catch (error) {
|
||||
setItems(activeAssets)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className="table">
|
||||
<thead className="table-borderless">
|
||||
<tr>
|
||||
<th className="font-weight-normal asset_row_name">Name</th>
|
||||
<th className="font-weight-normal" style={{ width: '21%' }}>
|
||||
Start
|
||||
</th>
|
||||
<th className="font-weight-normal" style={{ width: '21%' }}>
|
||||
End
|
||||
</th>
|
||||
<th className="font-weight-normal" style={{ width: '13%' }}>
|
||||
Duration
|
||||
</th>
|
||||
<th className="font-weight-normal" style={{ width: '7%' }}>
|
||||
Activity
|
||||
</th>
|
||||
<th className="font-weight-normal" style={{ width: '13%' }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div className="mb-1"></div>
|
||||
<table className="table">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<tbody id="active-assets" className="table-borderless">
|
||||
<SortableContext
|
||||
items={items.map((a) => a.asset_id.toString())}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{items.map((asset) => (
|
||||
<SortableAssetRow
|
||||
key={asset.asset_id}
|
||||
id={asset.asset_id.toString()}
|
||||
name={asset.name}
|
||||
startDate={asset.start_date}
|
||||
endDate={asset.end_date}
|
||||
duration={asset.duration}
|
||||
isEnabled={asset.is_enabled}
|
||||
assetId={asset.asset_id}
|
||||
isProcessing={asset.is_processing}
|
||||
uri={asset.uri}
|
||||
mimetype={asset.mimetype}
|
||||
nocache={asset.nocache}
|
||||
skipAssetCheck={asset.skip_asset_check}
|
||||
onEditAsset={onEditAsset}
|
||||
showDragHandle={true}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</tbody>
|
||||
</DndContext>
|
||||
</table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
5
static/src/components/add-asset-modal.jsx
Normal file
5
static/src/components/add-asset-modal.jsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { AssetModal } from './asset-modal'
|
||||
|
||||
export const AddAssetModal = (props) => {
|
||||
return <AssetModal {...props} />
|
||||
}
|
||||
14
static/src/components/alert.jsx
Normal file
14
static/src/components/alert.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export const Alert = ({ message }) => {
|
||||
return (
|
||||
<div id="request-error" className="navbar navbar fixed-top">
|
||||
<div className="container">
|
||||
<div className="alert">
|
||||
<button className="close" type="button">
|
||||
×
|
||||
</button>
|
||||
<span className="msg">{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
53
static/src/components/app.jsx
Normal file
53
static/src/components/app.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Routes, Route } from 'react-router'
|
||||
import { useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { fetchDeviceSettings } from '@/store/assets/asset-modal-slice'
|
||||
|
||||
import { Integrations } from '@/components/integrations'
|
||||
import { Navbar } from '@/components/navbar'
|
||||
import { ScheduleOverview } from '@/components/home'
|
||||
import { Settings } from '@/components/settings'
|
||||
import { SystemInfo } from '@/components/system-info'
|
||||
import { Footer } from '@/components/footer'
|
||||
|
||||
export const App = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDeviceSettings())
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<ScheduleOverview />} />
|
||||
<Route path="/integrations" element={<Integrations />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/system-info" element={<SystemInfo />} />
|
||||
</Routes>
|
||||
|
||||
<div className="container mt-4">
|
||||
<div className="row">
|
||||
<div className="col-6 small text-white">
|
||||
<span>
|
||||
Want to get more out of your digital signage?
|
||||
<a
|
||||
className="brand"
|
||||
href="https://www.screenly.io/?utm_source=Anthias&utm_medium=root-page&utm_campaign=UI"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{' '}
|
||||
<strong>Try Screenly</strong>.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
87
static/src/components/asset-modal/file-upload-tab.jsx
Normal file
87
static/src/components/asset-modal/file-upload-tab.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
/**
|
||||
* File upload tab component for the asset modal
|
||||
* @param {Object} props - Component props
|
||||
* @param {Object} props.fileInputRef - Reference to the file input
|
||||
* @param {Object} props.dropZoneRef - Reference to the drop zone
|
||||
* @param {Function} props.handleFileSelect - File select handler
|
||||
* @param {Function} props.handleFileDrop - File drop handler
|
||||
* @param {Function} props.handleDragOver - Drag over handler
|
||||
* @param {Function} props.handleDragEnter - Drag enter handler
|
||||
* @param {Function} props.handleDragLeave - Drag leave handler
|
||||
* @param {boolean} props.isSubmitting - Whether the form is submitting
|
||||
* @param {number} props.uploadProgress - Upload progress
|
||||
* @returns {JSX.Element} - File upload tab component
|
||||
*/
|
||||
export const FileUploadTab = ({
|
||||
fileInputRef,
|
||||
dropZoneRef,
|
||||
handleFileSelect,
|
||||
handleFileDrop,
|
||||
handleDragOver,
|
||||
handleDragEnter,
|
||||
handleDragLeave,
|
||||
isSubmitting,
|
||||
uploadProgress,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
id="tab-file_upload"
|
||||
className={classNames('tab-pane', {
|
||||
active: true,
|
||||
})}
|
||||
>
|
||||
<div className="control-group">
|
||||
<div
|
||||
className="filedrop"
|
||||
ref={dropZoneRef}
|
||||
onDrop={handleFileDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<div className="upload-header">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => fileInputRef.current.click()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Add Files
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
name="file_upload"
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileSelect}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<br />
|
||||
or
|
||||
</div>
|
||||
<div>drop files here to upload</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="progress active"
|
||||
style={{
|
||||
marginTop: '1.25rem',
|
||||
opacity: isSubmitting ? 1 : 0,
|
||||
maxHeight: isSubmitting ? '20px' : '0',
|
||||
overflow: 'hidden',
|
||||
transition: 'opacity 0.3s ease-in-out, max-height 0.3s ease-in-out',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bar progress-bar-striped progress-bar progress-bar-animated"
|
||||
style={{
|
||||
width: `${uploadProgress}%`,
|
||||
transition: 'width 0.3s ease-in-out',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
86
static/src/components/asset-modal/file-upload-utils.js
Normal file
86
static/src/components/asset-modal/file-upload-utils.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Utility functions for file upload handling
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the mimetype based on filename
|
||||
* @param {string} filename - The name of the file
|
||||
* @returns {string} - The mimetype of the file
|
||||
*/
|
||||
export const getMimetype = (filename) => {
|
||||
const viduris = ['rtsp', 'rtmp']
|
||||
const mimetypes = [
|
||||
[['jpe', 'jpg', 'jpeg', 'png', 'pnm', 'gif', 'bmp'], 'image'],
|
||||
[['avi', 'mkv', 'mov', 'mpg', 'mpeg', 'mp4', 'ts', 'flv'], 'video'],
|
||||
]
|
||||
const domains = [[['www.youtube.com', 'youtu.be'], 'youtube_asset']]
|
||||
|
||||
// Check if it's a streaming URL
|
||||
const scheme = filename.split(':')[0].toLowerCase()
|
||||
if (viduris.includes(scheme)) {
|
||||
return 'streaming'
|
||||
}
|
||||
|
||||
// Check if it's a domain-specific asset
|
||||
try {
|
||||
const domain = filename.split('//')[1].toLowerCase().split('/')[0]
|
||||
for (const [domainList, type] of domains) {
|
||||
if (domainList.includes(domain)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid URL format
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
try {
|
||||
const ext = filename.split('.').pop().toLowerCase()
|
||||
for (const [extList, type] of mimetypes) {
|
||||
if (extList.includes(ext)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// No extension found
|
||||
}
|
||||
|
||||
// Default to webpage
|
||||
return 'webpage'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration for a mimetype
|
||||
* @param {string} mimetype - The mimetype of the file
|
||||
* @param {number} defaultDuration - Default duration for webpage
|
||||
* @param {number} defaultStreamingDuration - Default duration for streaming
|
||||
* @returns {number} - The duration in seconds
|
||||
*/
|
||||
export const getDurationForMimetype = (
|
||||
mimetype,
|
||||
defaultDuration,
|
||||
defaultStreamingDuration,
|
||||
) => {
|
||||
if (mimetype === 'video') {
|
||||
return 0
|
||||
} else if (mimetype === 'streaming') {
|
||||
return defaultStreamingDuration
|
||||
} else {
|
||||
return defaultDuration
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default dates for an asset
|
||||
* @returns {Object} - Object containing start_date and end_date
|
||||
*/
|
||||
export const getDefaultDates = () => {
|
||||
const now = new Date()
|
||||
const endDate = new Date()
|
||||
endDate.setDate(endDate.getDate() + 30) // 30 days from now
|
||||
|
||||
return {
|
||||
start_date: now.toISOString(),
|
||||
end_date: endDate.toISOString(),
|
||||
}
|
||||
}
|
||||
193
static/src/components/asset-modal/index.jsx
Normal file
193
static/src/components/asset-modal/index.jsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { setActiveTab, selectAssetModalState } from '@/store/assets'
|
||||
import { useFileUpload } from './use-file-upload'
|
||||
import { useAssetForm } from './use-asset-form'
|
||||
import { useModalAnimation } from './use-modal-animation'
|
||||
import { UriTab } from './uri-tab'
|
||||
import { FileUploadTab } from './file-upload-tab'
|
||||
|
||||
/**
|
||||
* Asset modal component
|
||||
* @param {Object} props - Component props
|
||||
* @param {boolean} props.isOpen - Whether the modal is open
|
||||
* @param {Function} props.onClose - Callback function to call after closing
|
||||
* @param {Function} props.onSave - Callback function to call after saving
|
||||
* @param {Object} props.initialData - Initial data for the form
|
||||
* @returns {JSX.Element|null} - Asset modal component
|
||||
*/
|
||||
export const AssetModal = ({ isOpen, onClose, onSave, initialData = {} }) => {
|
||||
const dispatch = useDispatch()
|
||||
const { activeTab, statusMessage, uploadProgress } = useSelector(
|
||||
selectAssetModalState,
|
||||
)
|
||||
|
||||
// Use custom hooks
|
||||
const {
|
||||
fileInputRef,
|
||||
dropZoneRef,
|
||||
handleFileSelect,
|
||||
handleFileDrop,
|
||||
handleDragOver,
|
||||
handleDragEnter,
|
||||
handleDragLeave,
|
||||
} = useFileUpload()
|
||||
|
||||
const {
|
||||
formData,
|
||||
isValid,
|
||||
errorMessage,
|
||||
isSubmitting,
|
||||
handleInputChange,
|
||||
handleSubmit,
|
||||
} = useAssetForm(onSave, onClose)
|
||||
|
||||
const { isVisible, modalRef, handleClose } = useModalAnimation(
|
||||
isOpen,
|
||||
onClose,
|
||||
)
|
||||
|
||||
// Reset form data when modal is opened
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Form reset is handled by the useAssetForm hook
|
||||
}
|
||||
}, [isOpen, initialData])
|
||||
|
||||
if (!isOpen && !isVisible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('modal', {
|
||||
show: isOpen,
|
||||
fade: true,
|
||||
'd-block': isOpen,
|
||||
'modal-visible': isVisible,
|
||||
})}
|
||||
aria-hidden="true"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
style={{
|
||||
display: isOpen ? 'block' : 'none',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="modal-dialog"
|
||||
role="document"
|
||||
ref={modalRef}
|
||||
style={{
|
||||
transition: 'transform 0.3s ease-in-out',
|
||||
transform: isVisible ? 'translate(0, 0)' : 'translate(0, -25%)',
|
||||
}}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="form-horizontal">
|
||||
<div className="modal-header">
|
||||
<h3 id="modalLabel">Add Asset</h3>
|
||||
<button type="button" className="close" onClick={handleClose}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="asset-location add">
|
||||
<fieldset>
|
||||
<div className="tabbable">
|
||||
<ul className="nav nav-tabs" id="add-asset-nav-tabs">
|
||||
<li
|
||||
className={classNames(
|
||||
'tabnav-uri nav-item text-center',
|
||||
{ active: activeTab === 'uri' },
|
||||
)}
|
||||
>
|
||||
<a
|
||||
className="nav-link"
|
||||
href="#"
|
||||
onClick={() => dispatch(setActiveTab('uri'))}
|
||||
>
|
||||
URL
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className={classNames(
|
||||
'tabnav-file_upload nav-item text-center',
|
||||
{ active: activeTab === 'file_upload' },
|
||||
)}
|
||||
>
|
||||
<a
|
||||
className="nav-link upload-asset-tab"
|
||||
href="#"
|
||||
onClick={() => dispatch(setActiveTab('file_upload'))}
|
||||
>
|
||||
Upload
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="tab-content px-4 pt-2 pb-4">
|
||||
{activeTab === 'uri' ? (
|
||||
<UriTab
|
||||
formData={formData}
|
||||
isValid={isValid}
|
||||
errorMessage={errorMessage}
|
||||
isSubmitting={isSubmitting}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
) : (
|
||||
<FileUploadTab
|
||||
fileInputRef={fileInputRef}
|
||||
dropZoneRef={dropZoneRef}
|
||||
handleFileSelect={handleFileSelect}
|
||||
handleFileDrop={handleFileDrop}
|
||||
handleDragOver={handleDragOver}
|
||||
handleDragEnter={handleDragEnter}
|
||||
handleDragLeave={handleDragLeave}
|
||||
isSubmitting={isSubmitting}
|
||||
uploadProgress={uploadProgress}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<div
|
||||
className="status"
|
||||
style={{
|
||||
display:
|
||||
statusMessage && activeTab === 'file_upload'
|
||||
? 'block'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
{statusMessage}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-long cancel"
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Back to Assets
|
||||
</button>
|
||||
{activeTab === 'uri' && (
|
||||
<button
|
||||
id="save-asset"
|
||||
className="btn btn-primary btn-long"
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || !isValid}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
61
static/src/components/asset-modal/uri-tab.jsx
Normal file
61
static/src/components/asset-modal/uri-tab.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
/**
|
||||
* URI tab component for the asset modal
|
||||
* @param {Object} props - Component props
|
||||
* @param {Object} props.formData - Form data
|
||||
* @param {boolean} props.isValid - Whether the URI is valid
|
||||
* @param {string} props.errorMessage - Error message
|
||||
* @param {boolean} props.isSubmitting - Whether the form is submitting
|
||||
* @param {Function} props.handleInputChange - Input change handler
|
||||
* @returns {JSX.Element} - URI tab component
|
||||
*/
|
||||
export const UriTab = ({
|
||||
formData,
|
||||
isValid,
|
||||
errorMessage,
|
||||
isSubmitting,
|
||||
handleInputChange,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
id="tab-uri"
|
||||
className={classNames('tab-pane', {
|
||||
active: true,
|
||||
})}
|
||||
>
|
||||
<div className="form-group row uri">
|
||||
<label className="col-4 col-form-label">Asset URL</label>
|
||||
<div className="col-7 controls">
|
||||
<input
|
||||
className={classNames('form-control', {
|
||||
'is-invalid': !isValid && formData.uri,
|
||||
})}
|
||||
name="uri"
|
||||
value={formData.uri}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Public URL to this asset's location"
|
||||
type="text"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
{!isValid && formData.uri && (
|
||||
<div className="invalid-feedback">{errorMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group row skip_asset_check_checkbox">
|
||||
<label className="col-4 small">Skip asset check</label>
|
||||
<div className="col-7 is_enabled-skip_asset_check_checkbox checkbox">
|
||||
<input
|
||||
name="skipAssetCheck"
|
||||
type="checkbox"
|
||||
checked={formData.skipAssetCheck}
|
||||
onChange={handleInputChange}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
128
static/src/components/asset-modal/use-asset-form.js
Normal file
128
static/src/components/asset-modal/use-asset-form.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
updateFormData,
|
||||
validateUrl,
|
||||
setValid,
|
||||
setErrorMessage,
|
||||
resetForm,
|
||||
saveAsset,
|
||||
selectAssetModalState,
|
||||
} from '@/store/assets'
|
||||
import {
|
||||
getMimetype,
|
||||
getDurationForMimetype,
|
||||
getDefaultDates,
|
||||
} from './file-upload-utils'
|
||||
|
||||
/**
|
||||
* Custom hook for asset form handling
|
||||
* @param {Function} onSave - Callback function to call after successful save
|
||||
* @param {Function} onClose - Callback function to call after closing
|
||||
* @returns {Object} - Form handlers and state
|
||||
*/
|
||||
export const useAssetForm = (onSave, onClose) => {
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
activeTab,
|
||||
formData,
|
||||
isValid,
|
||||
errorMessage,
|
||||
statusMessage,
|
||||
isSubmitting,
|
||||
defaultDuration,
|
||||
defaultStreamingDuration,
|
||||
} = useSelector(selectAssetModalState)
|
||||
|
||||
/**
|
||||
* Handle input change
|
||||
* @param {Event} e - The input change event
|
||||
*/
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value, type, checked } = e.target
|
||||
dispatch(
|
||||
updateFormData({
|
||||
[name]: type === 'checkbox' ? checked : value,
|
||||
}),
|
||||
)
|
||||
|
||||
// Validate URL when it changes
|
||||
if (name === 'uri' && activeTab === 'uri') {
|
||||
dispatch(validateUrl(value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission
|
||||
* @param {Event} e - The form submission event
|
||||
*/
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (activeTab === 'uri') {
|
||||
if (!formData.uri) {
|
||||
dispatch(setErrorMessage('Please enter a URL'))
|
||||
dispatch(setValid(false))
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
|
||||
// Determine mimetype based on URL
|
||||
const mimetype = getMimetype(formData.uri)
|
||||
|
||||
// Get duration based on mimetype
|
||||
const duration = getDurationForMimetype(
|
||||
mimetype,
|
||||
defaultDuration,
|
||||
defaultStreamingDuration,
|
||||
)
|
||||
|
||||
// Get default dates
|
||||
const dates = getDefaultDates()
|
||||
|
||||
// Create asset data
|
||||
const assetData = {
|
||||
...formData,
|
||||
mimetype,
|
||||
name: formData.uri, // Use URI as name by default
|
||||
is_active: 1,
|
||||
is_enabled: 0,
|
||||
is_processing: 0,
|
||||
nocache: 0,
|
||||
play_order: 0,
|
||||
skip_asset_check: formData.skipAssetCheck ? 1 : 0,
|
||||
duration,
|
||||
...dates,
|
||||
}
|
||||
|
||||
try {
|
||||
// Save the asset
|
||||
const savedAsset = await dispatch(saveAsset({ assetData })).unwrap()
|
||||
|
||||
// Call the onSave callback with the asset data
|
||||
onSave(savedAsset)
|
||||
|
||||
// Reset form
|
||||
dispatch(resetForm())
|
||||
|
||||
// Close the modal
|
||||
onClose()
|
||||
} catch (error) {
|
||||
dispatch(setErrorMessage('Failed to save asset. Please try again.'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
formData,
|
||||
isValid,
|
||||
errorMessage,
|
||||
statusMessage,
|
||||
isSubmitting,
|
||||
handleInputChange,
|
||||
handleSubmit,
|
||||
}
|
||||
}
|
||||
140
static/src/components/asset-modal/use-file-upload.js
Normal file
140
static/src/components/asset-modal/use-file-upload.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useRef } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
uploadFile,
|
||||
saveAsset,
|
||||
setErrorMessage,
|
||||
setStatusMessage,
|
||||
setUploadProgress,
|
||||
resetForm,
|
||||
selectAssetModalState,
|
||||
} from '@/store/assets'
|
||||
|
||||
/**
|
||||
* Custom hook for file upload functionality
|
||||
* @returns {Object} - File upload handlers and refs
|
||||
*/
|
||||
export const useFileUpload = () => {
|
||||
const dispatch = useDispatch()
|
||||
const { formData } = useSelector(selectAssetModalState)
|
||||
const fileInputRef = useRef(null)
|
||||
const dropZoneRef = useRef(null)
|
||||
|
||||
/**
|
||||
* Handle file selection from input
|
||||
* @param {Event} e - The file input change event
|
||||
*/
|
||||
const handleFileSelect = (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (file) {
|
||||
handleFileUpload(file)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file drop
|
||||
* @param {Event} e - The drop event
|
||||
*/
|
||||
const handleFileDrop = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const file = e.dataTransfer.files[0]
|
||||
if (file) {
|
||||
handleFileUpload(file)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag over event
|
||||
* @param {Event} e - The drag over event
|
||||
*/
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag enter event
|
||||
* @param {Event} e - The drag enter event
|
||||
*/
|
||||
const handleDragEnter = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag leave event
|
||||
* @param {Event} e - The drag leave event
|
||||
*/
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Main file upload function
|
||||
* @param {File} file - The file to upload
|
||||
*/
|
||||
const handleFileUpload = async (file) => {
|
||||
try {
|
||||
// Upload the file
|
||||
const result = await dispatch(
|
||||
uploadFile({ file, skipAssetCheck: formData.skipAssetCheck }),
|
||||
).unwrap()
|
||||
|
||||
// Create asset data
|
||||
const assetData = {
|
||||
uri: result.fileData.uri,
|
||||
ext: result.fileData.ext,
|
||||
name: file.name,
|
||||
mimetype: result.mimetype,
|
||||
is_active: 1,
|
||||
is_enabled: 0,
|
||||
is_processing: 0,
|
||||
nocache: 0,
|
||||
play_order: 0,
|
||||
duration: result.duration,
|
||||
skip_asset_check: formData.skipAssetCheck ? 1 : 0,
|
||||
...result.dates,
|
||||
}
|
||||
|
||||
// Save the asset
|
||||
await dispatch(saveAsset({ assetData })).unwrap()
|
||||
|
||||
// Reset form and show success message
|
||||
dispatch(resetForm())
|
||||
dispatch(setStatusMessage('Upload completed.'))
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
|
||||
// Hide status message after 5 seconds
|
||||
setTimeout(() => {
|
||||
dispatch(setStatusMessage(''))
|
||||
}, 5000)
|
||||
} catch (error) {
|
||||
dispatch(setErrorMessage(`Upload failed: ${error.message}`))
|
||||
dispatch(setUploadProgress(0))
|
||||
|
||||
// Reset the progress bar width directly
|
||||
const progressBar = document.querySelector('.progress .bar')
|
||||
if (progressBar) {
|
||||
progressBar.style.width = '0%'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fileInputRef,
|
||||
dropZoneRef,
|
||||
handleFileSelect,
|
||||
handleFileDrop,
|
||||
handleDragOver,
|
||||
handleDragEnter,
|
||||
handleDragLeave,
|
||||
handleFileUpload,
|
||||
}
|
||||
}
|
||||
61
static/src/components/asset-modal/use-modal-animation.js
Normal file
61
static/src/components/asset-modal/use-modal-animation.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Custom hook for modal animation
|
||||
* @param {boolean} isOpen - Whether the modal is open
|
||||
* @param {Function} onClose - Callback function to call after closing
|
||||
* @returns {Object} - Modal animation state and handlers
|
||||
*/
|
||||
export const useModalAnimation = (isOpen, onClose) => {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [isClosing] = useState(false)
|
||||
const modalRef = useRef(null)
|
||||
|
||||
// Handle animation when modal opens/closes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Small delay to ensure the DOM is updated before adding the visible class
|
||||
setTimeout(() => {
|
||||
setIsVisible(true)
|
||||
}, 10)
|
||||
} else {
|
||||
setIsVisible(false)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
// Handle clicks outside the modal
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (modalRef.current && !modalRef.current.contains(event.target)) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
/**
|
||||
* Handle modal close
|
||||
*/
|
||||
const handleClose = () => {
|
||||
// Start the closing animation
|
||||
setIsVisible(false)
|
||||
// Wait for animation to complete before calling onClose
|
||||
setTimeout(() => {
|
||||
onClose()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
isClosing,
|
||||
modalRef,
|
||||
handleClose,
|
||||
}
|
||||
}
|
||||
534
static/src/components/asset-row.jsx
Normal file
534
static/src/components/asset-row.jsx
Normal file
@@ -0,0 +1,534 @@
|
||||
import {
|
||||
FaDownload,
|
||||
FaPencilAlt,
|
||||
FaTrashAlt,
|
||||
FaGlobe,
|
||||
FaImage,
|
||||
FaVideo,
|
||||
} from 'react-icons/fa'
|
||||
import { GiHamburgerMenu } from 'react-icons/gi'
|
||||
import Swal from 'sweetalert2'
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, forwardRef, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { css } from '@/utils'
|
||||
|
||||
import { toggleAssetEnabled, fetchAssets } from '@/store/assets'
|
||||
|
||||
const tooltipStyles = css`
|
||||
.tooltip {
|
||||
opacity: 1;
|
||||
transition: opacity 0s ease-in-out;
|
||||
}
|
||||
.tooltip.fade {
|
||||
opacity: 0;
|
||||
}
|
||||
.tooltip.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.tooltip-inner {
|
||||
background-color: #2c3e50;
|
||||
color: #fff;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.tooltip.bs-tooltip-top .arrow::before {
|
||||
border-top-color: #2c3e50;
|
||||
}
|
||||
|
||||
/* SweetAlert2 Custom Styles */
|
||||
html.swal2-shown body.swal2-shown {
|
||||
overflow-y: auto;
|
||||
padding-right: 0;
|
||||
}
|
||||
.swal2-popup.swal2-modal {
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.swal2-popup .swal2-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
.swal2-popup .swal2-html-container {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
.swal2-popup .swal2-confirm {
|
||||
background-color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
.swal2-popup .swal2-confirm:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
.swal2-popup .swal2-cancel {
|
||||
background-color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
.swal2-popup .swal2-cancel:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
.swal2-popup .swal2-actions {
|
||||
gap: 0.1rem;
|
||||
}
|
||||
.swal2-popup .swal2-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
.swal2-popup .swal2-icon-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`
|
||||
|
||||
const formatDuration = (seconds) => {
|
||||
let durationString = ''
|
||||
const secInt = parseInt(seconds)
|
||||
|
||||
const hours = Math.floor(secInt / 3600)
|
||||
if (hours > 0) {
|
||||
durationString += `${hours} hours `
|
||||
}
|
||||
|
||||
const minutes = Math.floor(secInt / 60) % 60
|
||||
if (minutes > 0) {
|
||||
durationString += `${minutes} min `
|
||||
}
|
||||
|
||||
const secs = secInt % 60
|
||||
if (secs > 0) {
|
||||
durationString += `${secs} sec`
|
||||
}
|
||||
|
||||
return durationString
|
||||
}
|
||||
|
||||
const formatDate = (date, dateFormat, use24HourClock = false) => {
|
||||
if (!date) return ''
|
||||
|
||||
// Create a Date object from the input date string
|
||||
const dateObj = new Date(date)
|
||||
|
||||
// Check if the date is valid
|
||||
if (isNaN(dateObj.getTime())) return date
|
||||
|
||||
// Extract the separator from the format
|
||||
const separator = dateFormat.includes('/')
|
||||
? '/'
|
||||
: dateFormat.includes('-')
|
||||
? '-'
|
||||
: dateFormat.includes('.')
|
||||
? '.'
|
||||
: '/'
|
||||
|
||||
// Extract the format parts from the dateFormat string
|
||||
const formatParts = dateFormat.split(/[\/\-\.]/)
|
||||
|
||||
// Set up the date formatting options
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: !use24HourClock, // Use 12-hour format if use24HourClock is false
|
||||
}
|
||||
|
||||
// Create a formatter with the specified options
|
||||
const formatter = new Intl.DateTimeFormat('en-US', options)
|
||||
|
||||
// Format the date and get the parts
|
||||
const formattedParts = formatter.formatToParts(dateObj)
|
||||
|
||||
// Extract the formatted values
|
||||
const month = formattedParts.find((p) => p.type === 'month').value
|
||||
const day = formattedParts.find((p) => p.type === 'day').value
|
||||
const year = formattedParts.find((p) => p.type === 'year').value
|
||||
const hour = formattedParts.find((p) => p.type === 'hour').value
|
||||
const minute = formattedParts.find((p) => p.type === 'minute').value
|
||||
const second = formattedParts.find((p) => p.type === 'second').value
|
||||
|
||||
// Get the period (AM/PM) if using 12-hour format
|
||||
let period = ''
|
||||
if (!use24HourClock) {
|
||||
const periodPart = formattedParts.find((p) => p.type === 'dayPeriod')
|
||||
if (periodPart) {
|
||||
period = ` ${periodPart.value}`
|
||||
}
|
||||
}
|
||||
|
||||
// Build the date part according to the format
|
||||
let datePart = ''
|
||||
|
||||
// Determine the order based on the format
|
||||
if (formatParts[0].includes('mm')) {
|
||||
datePart = `${month}${separator}${day}${separator}${year}`
|
||||
} else if (formatParts[0].includes('dd')) {
|
||||
datePart = `${day}${separator}${month}${separator}${year}`
|
||||
} else if (formatParts[0].includes('yyyy')) {
|
||||
datePart = `${year}${separator}${month}${separator}${day}`
|
||||
} else {
|
||||
// Default to mm/dd/yyyy if format is not recognized
|
||||
datePart = `${month}${separator}${day}${separator}${year}`
|
||||
}
|
||||
|
||||
// Add the time part with AM/PM if using 12-hour format
|
||||
const timePart = `${hour}:${minute}:${second}${period}`
|
||||
|
||||
return `${datePart} ${timePart}`
|
||||
}
|
||||
|
||||
const getMimetypeIcon = (mimetype) => {
|
||||
if (mimetype.includes('image')) {
|
||||
return (
|
||||
<FaImage
|
||||
className="mr-2 align-middle"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
)
|
||||
} else if (mimetype.includes('video')) {
|
||||
return (
|
||||
<FaVideo
|
||||
className="mr-2 align-middle"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
)
|
||||
} else if (mimetype.includes('webpage')) {
|
||||
return (
|
||||
<FaGlobe
|
||||
className="mr-2 align-middle"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<FaGlobe
|
||||
className="mr-2 align-middle"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const AssetRow = forwardRef((props, ref) => {
|
||||
const defaultDateFormat = 'mm/dd/yyyy'
|
||||
const dispatch = useDispatch()
|
||||
const [isDisabled, setIsDisabled] = useState(false)
|
||||
const [dateFormat, setDateFormat] = useState(defaultDateFormat)
|
||||
const [use24HourClock, setUse24HourClock] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDateFormat = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v2/device_settings')
|
||||
const data = await response.json()
|
||||
setDateFormat(data.date_format)
|
||||
setUse24HourClock(data.use_24_hour_clock)
|
||||
} catch (error) {
|
||||
setDateFormat(defaultDateFormat)
|
||||
setUse24HourClock(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchDateFormat()
|
||||
}, [])
|
||||
|
||||
const handleToggle = async () => {
|
||||
const newValue = !props.isEnabled ? 1 : 0
|
||||
setIsDisabled(true)
|
||||
try {
|
||||
await dispatch(
|
||||
toggleAssetEnabled({ assetId: props.assetId, newValue }),
|
||||
).unwrap()
|
||||
dispatch(fetchAssets())
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setIsDisabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = async (event) => {
|
||||
event.preventDefault()
|
||||
const assetId = props.assetId
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v2/assets/${assetId}/content`)
|
||||
const result = await response.json()
|
||||
|
||||
if (result.type === 'url') {
|
||||
window.open(result.url)
|
||||
} else if (result.type === 'file') {
|
||||
// Convert base64 to byte array
|
||||
const content = atob(result.content)
|
||||
const bytes = new Uint8Array(content.length)
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
bytes[i] = content.charCodeAt(i)
|
||||
}
|
||||
|
||||
const mimetype = result.mimetype
|
||||
const filename = result.filename
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([bytes], { type: mimetype })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
document.body.appendChild(a)
|
||||
a.download = filename
|
||||
a.href = url
|
||||
a.click()
|
||||
|
||||
// Clean up
|
||||
URL.revokeObjectURL(url)
|
||||
a.remove()
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: 'This action cannot be undone.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Delete',
|
||||
cancelButtonText: 'Cancel',
|
||||
reverseButtons: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonColor: '#6c757d',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
cancelButton: 'swal2-cancel',
|
||||
actions: 'swal2-actions',
|
||||
},
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
// Disable the row while deleting
|
||||
setIsDisabled(true)
|
||||
|
||||
// Make API call to delete the asset
|
||||
const response = await fetch(`/api/v2/assets/${props.assetId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Refresh the assets list after successful deletion
|
||||
dispatch(fetchAssets())
|
||||
|
||||
// Show success message
|
||||
Swal.fire({
|
||||
title: 'Deleted!',
|
||||
text: 'Asset has been deleted.',
|
||||
icon: 'success',
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Show error message
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
text: 'Failed to delete asset.',
|
||||
icon: 'error',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
text: 'Failed to delete asset.',
|
||||
icon: 'error',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
setIsDisabled(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
if (props.onEditAsset) {
|
||||
props.onEditAsset({
|
||||
id: props.assetId,
|
||||
name: props.name,
|
||||
start_date: props.startDate,
|
||||
end_date: props.endDate,
|
||||
duration: props.duration,
|
||||
uri: props.uri,
|
||||
mimetype: props.mimetype,
|
||||
is_enabled: props.isEnabled,
|
||||
nocache: props.nocache,
|
||||
skip_asset_check: props.skipAssetCheck,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{tooltipStyles}</style>
|
||||
<tr
|
||||
ref={ref}
|
||||
style={props.style}
|
||||
className={classNames({ warning: isDisabled })}
|
||||
>
|
||||
<td className={classNames('asset_row_name')}>
|
||||
{props.showDragHandle && (
|
||||
<span
|
||||
{...props.dragHandleProps}
|
||||
style={{
|
||||
cursor: props.isDragging ? 'grabbing' : 'grab',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
<GiHamburgerMenu
|
||||
className="mr-3 align-middle"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{getMimetypeIcon(props.mimetype)}
|
||||
<span
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
title={props.name}
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
>
|
||||
{props.name}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
style={{ width: '21%', maxWidth: '200px' }}
|
||||
className="text-truncate"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
title={formatDate(props.startDate, dateFormat, use24HourClock)}
|
||||
>
|
||||
{formatDate(props.startDate, dateFormat, use24HourClock)}
|
||||
</td>
|
||||
<td
|
||||
style={{ width: '21%', maxWidth: '200px' }}
|
||||
className="text-truncate"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
title={formatDate(props.endDate, dateFormat, use24HourClock)}
|
||||
>
|
||||
{formatDate(props.endDate, dateFormat, use24HourClock)}
|
||||
</td>
|
||||
<td
|
||||
style={{ width: '13%', maxWidth: '150px' }}
|
||||
className={classNames('text-truncate')}
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
title={formatDuration(props.duration)}
|
||||
>
|
||||
{formatDuration(props.duration)}
|
||||
</td>
|
||||
<td className={classNames('asset-toggle')} style={{ width: '7%' }}>
|
||||
<label
|
||||
className={classNames(
|
||||
'is_enabled-toggle',
|
||||
'toggle',
|
||||
'switch-light',
|
||||
'switch-material',
|
||||
'small',
|
||||
'm-0',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.isEnabled}
|
||||
onChange={handleToggle}
|
||||
disabled={isDisabled || props.isProcessing === 1}
|
||||
/>
|
||||
<span>
|
||||
<span className="off"></span>
|
||||
<span className="on"></span>
|
||||
<a></a>
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td className={classNames('asset_row_btns')}>
|
||||
<button
|
||||
className={classNames(
|
||||
'download-asset-button',
|
||||
'btn',
|
||||
'btn-outline-dark',
|
||||
'mr-1',
|
||||
'd-inline-flex',
|
||||
'p-2',
|
||||
)}
|
||||
type="button"
|
||||
disabled={isDisabled}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<FaDownload />
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
'edit-asset-button',
|
||||
'btn',
|
||||
'btn-outline-dark',
|
||||
'mr-1',
|
||||
'd-inline-flex',
|
||||
'p-2',
|
||||
)}
|
||||
type="button"
|
||||
disabled={isDisabled}
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<FaPencilAlt />
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
'delete-asset-button',
|
||||
'btn',
|
||||
'btn-outline-dark',
|
||||
'd-inline-flex',
|
||||
'p-2',
|
||||
)}
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<FaTrashAlt />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)
|
||||
})
|
||||
389
static/src/components/edit-asset-modal.jsx
Normal file
389
static/src/components/edit-asset-modal.jsx
Normal file
@@ -0,0 +1,389 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { fetchAssets } from '@/store/assets'
|
||||
|
||||
/**
|
||||
* Edit Asset Modal component
|
||||
* @param {Object} props - Component props
|
||||
* @param {boolean} props.isOpen - Whether the modal is open
|
||||
* @param {Function} props.onClose - Callback function to call after closing
|
||||
* @param {Object} props.asset - The asset to edit
|
||||
* @returns {JSX.Element|null} - Edit Asset Modal component
|
||||
*/
|
||||
export const EditAssetModal = ({ isOpen, onClose, asset }) => {
|
||||
const dispatch = useDispatch()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
duration: '',
|
||||
mimetype: 'webpage',
|
||||
nocache: false,
|
||||
skip_asset_check: false,
|
||||
})
|
||||
const [loopTimes, setLoopTimes] = useState('manual')
|
||||
const [startDateDate, setStartDateDate] = useState('')
|
||||
const [startDateTime, setStartDateTime] = useState('')
|
||||
const [endDateDate, setEndDateDate] = useState('')
|
||||
const [endDateTime, setEndDateTime] = useState('')
|
||||
|
||||
// Initialize form data when asset changes
|
||||
useEffect(() => {
|
||||
if (asset) {
|
||||
// Parse dates from UTC
|
||||
const startDate = new Date(asset.start_date)
|
||||
const endDate = new Date(asset.end_date)
|
||||
|
||||
// Format date and time parts in local timezone
|
||||
const formatDatePart = (date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const formatTimePart = (date) => {
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
setFormData({
|
||||
name: asset.name || '',
|
||||
start_date: asset.start_date || '',
|
||||
end_date: asset.end_date || '',
|
||||
duration: asset.duration || '',
|
||||
mimetype: asset.mimetype || 'webpage',
|
||||
nocache: asset.nocache || false,
|
||||
skip_asset_check: asset.skip_asset_check || false,
|
||||
})
|
||||
|
||||
setStartDateDate(formatDatePart(startDate))
|
||||
setStartDateTime(formatTimePart(startDate))
|
||||
setEndDateDate(formatDatePart(endDate))
|
||||
setEndDateTime(formatTimePart(endDate))
|
||||
}
|
||||
}, [asset])
|
||||
|
||||
// Handle modal visibility
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsVisible(true)
|
||||
} else {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false)
|
||||
}, 300) // Match the transition duration
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false)
|
||||
setTimeout(() => {
|
||||
onClose()
|
||||
}, 300) // Match the transition duration
|
||||
}
|
||||
|
||||
const handleModalClick = (e) => {
|
||||
// Only close if clicking the modal backdrop (outside the modal content)
|
||||
if (e.target === e.currentTarget) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value, type, checked } = e.target
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: type === 'checkbox' ? checked : value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleLoopTimesChange = (e) => {
|
||||
setLoopTimes(e.target.value)
|
||||
}
|
||||
|
||||
const handleDateChange = (e, type) => {
|
||||
const { value } = e.target
|
||||
if (type === 'startDate') {
|
||||
setStartDateDate(value)
|
||||
} else if (type === 'startTime') {
|
||||
setStartDateTime(value)
|
||||
} else if (type === 'endDate') {
|
||||
setEndDateDate(value)
|
||||
} else if (type === 'endTime') {
|
||||
setEndDateTime(value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
// Combine date and time parts
|
||||
const startDate = new Date(`${startDateDate}T${startDateTime}`)
|
||||
const endDate = new Date(`${endDateDate}T${endDateTime}`)
|
||||
|
||||
// Prepare data for API
|
||||
const updatedAsset = {
|
||||
...formData,
|
||||
start_date: startDate.toISOString(),
|
||||
end_date: endDate.toISOString(),
|
||||
asset_id: asset.id,
|
||||
is_enabled: asset.is_enabled,
|
||||
}
|
||||
|
||||
// Make API call to update asset
|
||||
const response = await fetch(`/api/v2/assets/${asset.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedAsset),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update asset')
|
||||
}
|
||||
|
||||
// Refresh assets list
|
||||
dispatch(fetchAssets())
|
||||
|
||||
// Close modal
|
||||
onClose()
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen && !isVisible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('modal', {
|
||||
show: isOpen,
|
||||
fade: true,
|
||||
'd-block': isOpen,
|
||||
'modal-visible': isVisible,
|
||||
})}
|
||||
aria-hidden="true"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
onClick={handleModalClick}
|
||||
style={{
|
||||
display: isOpen ? 'block' : 'none',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="modal-dialog"
|
||||
role="document"
|
||||
style={{
|
||||
transition: 'transform 0.3s ease-in-out',
|
||||
transform: isVisible ? 'translate(0, 0)' : 'translate(0, -25%)',
|
||||
}}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="form-horizontal">
|
||||
<div className="modal-header">
|
||||
<h3 id="modalLabel">Edit Asset</h3>
|
||||
<button type="button" className="close" onClick={handleClose}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="asset-location edit">
|
||||
<form id="edit-form" onSubmit={handleSubmit}>
|
||||
<div className="form-group row name">
|
||||
<label className="col-4 col-form-label">Name</label>
|
||||
<div className="col-7">
|
||||
<input
|
||||
className="form-control"
|
||||
name="name"
|
||||
placeholder="Nickname for this asset"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group row">
|
||||
<label className="col-4 col-form-label">
|
||||
Asset Location
|
||||
</label>
|
||||
<div className="col-8 controls">
|
||||
<div
|
||||
className="uri-text first text-break"
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{asset?.uri || ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group row mimetype">
|
||||
<label className="col-4 col-form-label">Asset Type</label>
|
||||
<div className="col-4 controls">
|
||||
<select
|
||||
className="mime-select form-control"
|
||||
name="mimetype"
|
||||
value={formData.mimetype}
|
||||
onChange={handleInputChange}
|
||||
disabled={true}
|
||||
>
|
||||
<option value="webpage">Webpage</option>
|
||||
<option value="image">Image</option>
|
||||
<option value="video">Video</option>
|
||||
<option value="streaming">Streaming</option>
|
||||
<option value="youtube_asset">YouTubeAsset</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="row form-group loop_date">
|
||||
<label className="col-4 col-form-label">Play for</label>
|
||||
<div className="controls col-7">
|
||||
<select
|
||||
className="form-control"
|
||||
id="loop_times"
|
||||
value={loopTimes}
|
||||
onChange={handleLoopTimesChange}
|
||||
>
|
||||
<option value="day">1 Day</option>
|
||||
<option value="week">1 Week</option>
|
||||
<option value="month">1 Month</option>
|
||||
<option value="year">1 Year</option>
|
||||
<option value="forever">Forever</option>
|
||||
<option value="manual">Manual</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="manul_date">
|
||||
<div className="form-group row start_date">
|
||||
<label className="col-4 col-form-label">Start Date</label>
|
||||
<div className="controls col-7">
|
||||
<input
|
||||
className="form-control date"
|
||||
name="start_date_date"
|
||||
type="date"
|
||||
value={startDateDate}
|
||||
onChange={(e) => handleDateChange(e, 'startDate')}
|
||||
style={{ marginRight: '5px' }}
|
||||
/>
|
||||
<input
|
||||
className="form-control time"
|
||||
name="start_date_time"
|
||||
type="time"
|
||||
value={startDateTime}
|
||||
onChange={(e) => handleDateChange(e, 'startTime')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group row end_date">
|
||||
<label className="col-4 col-form-label">End Date</label>
|
||||
<div className="controls col-7">
|
||||
<input
|
||||
className="form-control date"
|
||||
name="end_date_date"
|
||||
type="date"
|
||||
value={endDateDate}
|
||||
onChange={(e) => handleDateChange(e, 'endDate')}
|
||||
style={{ marginRight: '5px' }}
|
||||
/>
|
||||
<input
|
||||
className="form-control time"
|
||||
name="end_date_time"
|
||||
type="time"
|
||||
value={endDateTime}
|
||||
onChange={(e) => handleDateChange(e, 'endTime')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group row duration">
|
||||
<label className="col-4 col-form-label">Duration</label>
|
||||
<div className="col-7 controls">
|
||||
<input
|
||||
className="form-control"
|
||||
name="duration"
|
||||
type="number"
|
||||
value={formData.duration}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
seconds
|
||||
</div>
|
||||
</div>
|
||||
<div className="advanced-accordion accordion">
|
||||
<div className="accordion-group">
|
||||
<div className="accordion-heading">
|
||||
<i className="fas fa-play unrotated"></i>
|
||||
<a className="advanced-toggle" href="#">
|
||||
Advanced
|
||||
</a>
|
||||
</div>
|
||||
<div className="collapse-advanced accordion-body collapse">
|
||||
<div className="accordion-inner">
|
||||
<div className="form-group row">
|
||||
<label className="col-4 col-form-label">
|
||||
Disable cache
|
||||
</label>
|
||||
<div className="col-8 nocache controls justify-content-center align-self-center">
|
||||
<label className="nocache-toggle toggle switch-light switch-ios small m-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="nocache"
|
||||
checked={formData.nocache}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<a></a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<div
|
||||
className="float-left progress active"
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
<div className="bar progress-bar-striped progress-bar progress-bar-animated"></div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-long cancel"
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
id="save-asset"
|
||||
className="btn btn-primary btn-long"
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
static/src/components/empty-asset-message.jsx
Normal file
11
static/src/components/empty-asset-message.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export const EmptyAssetMessage = ({ onAddAssetClick }) => {
|
||||
return (
|
||||
<div className="table-assets-help-text">
|
||||
Currently there are no assets.
|
||||
<a className="add-asset-button" href="#" onClick={onAddAssetClick}>
|
||||
Add asset
|
||||
</a>{' '}
|
||||
now.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
80
static/src/components/footer.jsx
Normal file
80
static/src/components/footer.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<footer id="footer">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div id="screenly-logo" className="col-12 row m-2 ml-0 mr-0">
|
||||
<div className="links offset-3 col-6 text-center justify-content-center align-self-center">
|
||||
<a
|
||||
href="/api/docs/"
|
||||
target="_blank"
|
||||
className="mr-4 small"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
API
|
||||
</a>
|
||||
<a
|
||||
href="https://anthias.screenly.io/#faq?utm_source=Anthias&utm_medium=footer&utm_campaign=UI"
|
||||
target="_blank"
|
||||
className="mr-4 small"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
FAQ
|
||||
</a>
|
||||
<a
|
||||
href="https://screenly.io/?utm_source=Anthias&utm_medium=footer&utm_campaign=UI"
|
||||
target="_blank"
|
||||
className="mr-4 small"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Screenly.io
|
||||
</a>
|
||||
<a
|
||||
href="https://forums.screenly.io/"
|
||||
target="_blank"
|
||||
className="mr-4 small"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Support
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="github-stars"
|
||||
className="col-3 text-right justify-content-center align-self-center"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Screenly/Anthias"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
alt="GitHub Repo stars"
|
||||
src={(() => {
|
||||
const url = new URL(
|
||||
'https://img.shields.io/github/stars/Screenly/Anthias',
|
||||
)
|
||||
const params = new URLSearchParams({
|
||||
style: 'for-the-badge',
|
||||
labelColor: '#EBF0F4',
|
||||
color: '#FFE11A',
|
||||
logo: 'github',
|
||||
logoColor: 'black',
|
||||
})
|
||||
url.search = params.toString()
|
||||
return url.toString()
|
||||
})()}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="copy pb-4">
|
||||
<div className="container">
|
||||
<div className="text-center p-2">© Screenly, Inc.</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
259
static/src/components/home.jsx
Normal file
259
static/src/components/home.jsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import $ from 'jquery'
|
||||
import 'bootstrap/js/dist/tooltip'
|
||||
import {
|
||||
fetchAssets,
|
||||
selectActiveAssets,
|
||||
selectInactiveAssets,
|
||||
} from '@/store/assets'
|
||||
|
||||
import { EmptyAssetMessage } from '@/components/empty-asset-message'
|
||||
import { InactiveAssetsTable } from '@/components/inactive-assets'
|
||||
import { ActiveAssetsTable } from '@/components/active-assets'
|
||||
import { AddAssetModal } from '@/components/add-asset-modal'
|
||||
import { EditAssetModal } from '@/components/edit-asset-modal'
|
||||
|
||||
const tooltipStyles = `
|
||||
.tooltip {
|
||||
opacity: 1 !important;
|
||||
transition: opacity 0s ease-in-out !important;
|
||||
}
|
||||
.tooltip.fade {
|
||||
opacity: 0;
|
||||
}
|
||||
.tooltip.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.tooltip-inner {
|
||||
background-color: #2c3e50;
|
||||
color: #fff;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.tooltip.bs-tooltip-top .arrow::before {
|
||||
border-top-color: #2c3e50;
|
||||
}
|
||||
`
|
||||
|
||||
export const ScheduleOverview = () => {
|
||||
const dispatch = useDispatch()
|
||||
const activeAssets = useSelector(selectActiveAssets)
|
||||
const inactiveAssets = useSelector(selectInactiveAssets)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||
const [assetToEdit, setAssetToEdit] = useState(null)
|
||||
const [playerName, setPlayerName] = useState('')
|
||||
|
||||
const fetchPlayerName = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v2/device_settings')
|
||||
const data = await response.json()
|
||||
setPlayerName(data.player_name || '')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Schedule Overview'
|
||||
dispatch(fetchAssets())
|
||||
fetchPlayerName()
|
||||
}, [dispatch])
|
||||
|
||||
// Initialize tooltips
|
||||
useEffect(() => {
|
||||
const initializeTooltips = () => {
|
||||
$('[data-toggle="tooltip"]').tooltip({
|
||||
placement: 'top',
|
||||
trigger: 'hover',
|
||||
html: true,
|
||||
delay: { show: 0, hide: 0 },
|
||||
animation: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Initial tooltip initialization
|
||||
initializeTooltips()
|
||||
|
||||
// Reinitialize tooltips when assets change
|
||||
const observer = new MutationObserver(() => {
|
||||
initializeTooltips()
|
||||
})
|
||||
|
||||
// Observe changes in both active and inactive sections
|
||||
const activeSection = document.getElementById('active-assets-section')
|
||||
const inactiveSection = document.getElementById('inactive-assets-section')
|
||||
|
||||
if (activeSection) {
|
||||
observer.observe(activeSection, { childList: true, subtree: true })
|
||||
}
|
||||
if (inactiveSection) {
|
||||
observer.observe(inactiveSection, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
$('[data-toggle="tooltip"]').tooltip('dispose')
|
||||
}
|
||||
}, [activeAssets, inactiveAssets])
|
||||
|
||||
const handleAddAsset = (event) => {
|
||||
event.preventDefault()
|
||||
setIsModalOpen(true)
|
||||
setAssetToEdit(null)
|
||||
}
|
||||
|
||||
const handlePreviousAsset = async (event) => {
|
||||
event.preventDefault()
|
||||
await fetch('/api/v2/assets/control/previous')
|
||||
}
|
||||
|
||||
const handleNextAsset = async (event) => {
|
||||
event.preventDefault()
|
||||
await fetch('/api/v2/assets/control/next')
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
const handleSaveAsset = () => {
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
const handleEditAsset = (asset) => {
|
||||
setAssetToEdit(asset)
|
||||
setIsEditModalOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseEditModal = () => {
|
||||
setIsEditModalOpen(false)
|
||||
setAssetToEdit(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{tooltipStyles}</style>
|
||||
<div className="container pt-3 pb-3">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<h4 className="d-flex">
|
||||
<b
|
||||
className={classNames(
|
||||
'justify-content-center',
|
||||
'align-self-center',
|
||||
'text-white',
|
||||
)}
|
||||
>
|
||||
Schedule Overview
|
||||
</b>
|
||||
<div className="ml-auto">
|
||||
<a
|
||||
id="previous-asset-button"
|
||||
className={classNames(
|
||||
'btn',
|
||||
'btn-long',
|
||||
'btn-outline-primary',
|
||||
'mr-1',
|
||||
)}
|
||||
href="#"
|
||||
onClick={handlePreviousAsset}
|
||||
>
|
||||
<i className="fas fa-chevron-left pr-2"></i>
|
||||
Previous Asset
|
||||
</a>
|
||||
<a
|
||||
id="next-asset-button"
|
||||
className={classNames(
|
||||
'btn',
|
||||
'btn-long',
|
||||
'btn-outline-primary',
|
||||
'mr-1',
|
||||
)}
|
||||
href="#"
|
||||
onClick={handleNextAsset}
|
||||
>
|
||||
Next Asset
|
||||
<i className="fas fa-chevron-right pl-2"></i>
|
||||
</a>
|
||||
<a
|
||||
id="add-asset-button"
|
||||
className={classNames(
|
||||
'add-asset-button',
|
||||
'btn',
|
||||
'btn-long',
|
||||
'btn-primary',
|
||||
'mr-1',
|
||||
)}
|
||||
href="#"
|
||||
onClick={handleAddAsset}
|
||||
>
|
||||
Add Asset
|
||||
</a>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
{playerName && (
|
||||
<span className="badge badge-primary px-3 py-2 rounded-pill mb-3">
|
||||
<h6 className="my-0 text-center font-weight-bold">
|
||||
{playerName}
|
||||
</h6>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="assets">
|
||||
<div className="container">
|
||||
<div className="row content active-content px-2 pt-4">
|
||||
<div className="col-12 mb-5">
|
||||
<section id="active-assets-section">
|
||||
<h5>
|
||||
<b>Active assets</b>
|
||||
</h5>
|
||||
<ActiveAssetsTable onEditAsset={handleEditAsset} />
|
||||
{activeAssets.length === 0 && (
|
||||
<EmptyAssetMessage onAddAssetClick={handleAddAsset} />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mt-4">
|
||||
<div className="row content inactive-content px-2 pt-4">
|
||||
<div className="col-12 mb-5">
|
||||
<section id="inactive-assets-section">
|
||||
<h5>
|
||||
<b>Inactive assets</b>
|
||||
</h5>
|
||||
<InactiveAssetsTable onEditAsset={handleEditAsset} />
|
||||
{inactiveAssets.length === 0 && (
|
||||
<EmptyAssetMessage onAddAssetClick={handleAddAsset} />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<AddAssetModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSaveAsset}
|
||||
initialData={assetToEdit}
|
||||
/>
|
||||
|
||||
<EditAssetModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={handleCloseEditModal}
|
||||
asset={assetToEdit}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
72
static/src/components/inactive-assets.jsx
Normal file
72
static/src/components/inactive-assets.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectInactiveAssets } from '@/store/assets'
|
||||
import { AssetRow } from '@/components/asset-row'
|
||||
|
||||
export const InactiveAssetsTable = ({ onEditAsset }) => {
|
||||
const inactiveAssets = useSelector(selectInactiveAssets)
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className="table">
|
||||
<thead className="table-borderless">
|
||||
<tr>
|
||||
<th className="text-secondary font-weight-normal asset_row_name">
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
className="text-secondary font-weight-normal"
|
||||
style={{ width: '21%' }}
|
||||
>
|
||||
Start
|
||||
</th>
|
||||
<th
|
||||
className="text-secondary font-weight-normal"
|
||||
style={{ width: '21%' }}
|
||||
>
|
||||
End
|
||||
</th>
|
||||
<th
|
||||
className="text-secondary font-weight-normal"
|
||||
style={{ width: '13%' }}
|
||||
>
|
||||
Duration
|
||||
</th>
|
||||
<th
|
||||
className="text-secondary font-weight-normal"
|
||||
style={{ width: '7%' }}
|
||||
>
|
||||
Activity
|
||||
</th>
|
||||
<th
|
||||
className="text-secondary font-weight-normal"
|
||||
style={{ width: '13%' }}
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div className="mb-1"></div>
|
||||
<table className="table">
|
||||
<tbody id="inactive-assets" className="table-borderless">
|
||||
{inactiveAssets.map((asset) => (
|
||||
<AssetRow
|
||||
key={asset.asset_id}
|
||||
name={asset.name}
|
||||
startDate={asset.start_date}
|
||||
endDate={asset.end_date}
|
||||
duration={asset.duration}
|
||||
isEnabled={asset.is_enabled}
|
||||
assetId={asset.asset_id}
|
||||
isProcessing={asset.is_processing}
|
||||
uri={asset.uri}
|
||||
mimetype={asset.mimetype}
|
||||
nocache={asset.nocache}
|
||||
skipAssetCheck={asset.skip_asset_check}
|
||||
onEditAsset={onEditAsset}
|
||||
showDragHandle={false}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
104
static/src/components/integrations.jsx
Normal file
104
static/src/components/integrations.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const Integrations = () => {
|
||||
const [data, setData] = useState({
|
||||
is_balena: false,
|
||||
balena_device_id: '',
|
||||
balena_app_id: '',
|
||||
balena_app_name: '',
|
||||
balena_supervisor_version: '',
|
||||
balena_host_os_version: '',
|
||||
balena_device_name_at_init: '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Integrations'
|
||||
fetch('/api/v2/integrations')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setData(data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row py-2">
|
||||
<div className="col-12">
|
||||
<h4 className="page-header text-white">
|
||||
<b>Integrations</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row content" style={{ minHeight: '60vh' }}>
|
||||
{data.is_balena && (
|
||||
<div id="balena-section" className="col-12">
|
||||
<h4 className="page-header">
|
||||
<b>Balena</b>
|
||||
</h4>
|
||||
<table className="table">
|
||||
<thead className="table-borderless">
|
||||
<tr>
|
||||
<th className="text-secondary font-weight-normal" scope="col">
|
||||
Option
|
||||
</th>
|
||||
<th className="text-secondary font-weight-normal" scope="col">
|
||||
Value
|
||||
</th>
|
||||
<th className="text-secondary font-weight-normal" scope="col">
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Device Name</th>
|
||||
<td>{data.balena_device_name_at_init}</td>
|
||||
<td>The name of the device on first initialisation.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Device UUID</th>
|
||||
<td>{data.balena_device_id}</td>
|
||||
<td>
|
||||
The unique identification number for the device. This is
|
||||
used to identify it on balena.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">App ID</th>
|
||||
<td>{data.balena_app_id}</td>
|
||||
<td>
|
||||
ID number of the balena application the device is
|
||||
associated.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">App Name</th>
|
||||
<td>{data.balena_app_name}</td>
|
||||
<td>
|
||||
The name of the balena application the device is associated
|
||||
with.
|
||||
</td>
|
||||
</tr>
|
||||
{data.balena_supervisor_version && (
|
||||
<tr>
|
||||
<th scope="row">Supervisor Version</th>
|
||||
<td>{data.balena_supervisor_version}</td>
|
||||
<td>
|
||||
The current version of the supervisor agent running on the
|
||||
device.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<th scope="row">Host OS Version</th>
|
||||
<td>{data.balena_host_os_version}</td>
|
||||
<td>The version of the host OS.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
static/src/components/navbar.jsx
Normal file
103
static/src/components/navbar.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
FaArrowCircleDown,
|
||||
FaRegClock,
|
||||
FaCog,
|
||||
FaPlusSquare,
|
||||
FaTasks,
|
||||
} from 'react-icons/fa'
|
||||
import { Link, NavLink } from 'react-router'
|
||||
|
||||
export const Navbar = () => {
|
||||
const [upToDate, setUpToDate] = useState(null)
|
||||
const [isBalena, setIsBalena] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [integrationsResponse, infoResponse] = await Promise.all([
|
||||
fetch('/api/v2/integrations'),
|
||||
fetch('/api/v2/info'),
|
||||
])
|
||||
|
||||
const [integrationsData, infoData] = await Promise.all([
|
||||
integrationsResponse.json(),
|
||||
infoResponse.json(),
|
||||
])
|
||||
|
||||
setIsBalena(integrationsData.is_balena)
|
||||
setUpToDate(infoData.up_to_date)
|
||||
} catch (error) {
|
||||
setIsBalena(false)
|
||||
setUpToDate(false)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="navbar navbar-header navbar-expand-lg fixed-top bg-dark">
|
||||
<div className="container">
|
||||
<NavLink to="/" className="brand">
|
||||
<img src="/static/img/logo-full.svg" />
|
||||
</NavLink>
|
||||
<ul className="nav float-right">
|
||||
{!isLoading && !upToDate && !isBalena && (
|
||||
<li className="update-available">
|
||||
<Link to="/settings#upgrade-section">
|
||||
<span className="pr-1">
|
||||
<FaArrowCircleDown />
|
||||
</span>
|
||||
Update Available
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li>
|
||||
<NavLink to="/">
|
||||
<span className="pr-1">
|
||||
<FaRegClock />
|
||||
</span>
|
||||
Schedule Overview
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
{isBalena && (
|
||||
<li>
|
||||
<NavLink to="/integrations">
|
||||
<span className="pr-1">
|
||||
<FaPlusSquare />
|
||||
</span>
|
||||
Integrations
|
||||
</NavLink>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li>
|
||||
<NavLink to="/settings">
|
||||
<span className="pr-1">
|
||||
<FaCog />
|
||||
</span>
|
||||
Settings
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="divider-vertical"></li>
|
||||
<li>
|
||||
<NavLink to="/system-info">
|
||||
<span className="pr-1">
|
||||
<FaTasks />
|
||||
</span>
|
||||
System Info
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
774
static/src/components/settings.jsx
Normal file
774
static/src/components/settings.jsx
Normal file
@@ -0,0 +1,774 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { fetchDeviceSettings } from '@/store/assets/asset-modal-slice'
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
export const Settings = () => {
|
||||
const dispatch = useDispatch()
|
||||
const [settings, setSettings] = useState({
|
||||
playerName: '',
|
||||
defaultDuration: 0,
|
||||
defaultStreamingDuration: 0,
|
||||
audioOutput: 'hdmi',
|
||||
dateFormat: 'mm/dd/yyyy',
|
||||
authBackend: '',
|
||||
currentPassword: '',
|
||||
user: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
showSplash: false,
|
||||
defaultAssets: false,
|
||||
shufflePlaylist: false,
|
||||
use24HourClock: false,
|
||||
debugLogging: false,
|
||||
})
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [prevAuthBackend, setPrevAuthBackend] = useState('')
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
|
||||
const handleBackup = async () => {
|
||||
const backupButton = document.getElementById('btn-backup')
|
||||
const originalText = backupButton.textContent
|
||||
backupButton.textContent = 'Preparing archive...'
|
||||
backupButton.disabled = true
|
||||
document.getElementById('btn-upload').disabled = true
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v2/backup', {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create backup')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data) {
|
||||
window.location = `/static_with_mime/${data}?mime=application/x-tgz`
|
||||
}
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
title: 'Error!',
|
||||
text:
|
||||
err.message ||
|
||||
'The operation failed. Please reload the page and try again.',
|
||||
icon: 'error',
|
||||
confirmButtonColor: '#dc3545',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
backupButton.textContent = originalText
|
||||
backupButton.disabled = false
|
||||
document.getElementById('btn-upload').disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = (e) => {
|
||||
e.preventDefault()
|
||||
const fileInput = document.querySelector('[name="backup_upload"]')
|
||||
fileInput.value = '' // Reset the file input
|
||||
fileInput.click()
|
||||
}
|
||||
|
||||
const handleFileUpload = async (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
setIsUploading(true)
|
||||
setUploadProgress(0)
|
||||
document.getElementById('btn-upload').style.display = 'none'
|
||||
document.getElementById('btn-backup').style.display = 'none'
|
||||
document.querySelector('.progress').style.display = 'block'
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('backup_upload', file)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v2/recover', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total,
|
||||
)
|
||||
setUploadProgress(percentCompleted)
|
||||
document.querySelector('.progress .bar').style.width =
|
||||
`${percentCompleted}%`
|
||||
document.querySelector('.progress .bar').textContent =
|
||||
`Uploading: ${percentCompleted}%`
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to upload backup')
|
||||
}
|
||||
|
||||
if (data) {
|
||||
await Swal.fire({
|
||||
title: 'Success!',
|
||||
text:
|
||||
typeof data === 'string' ? data : 'Backup uploaded successfully',
|
||||
icon: 'success',
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
})
|
||||
|
||||
// Fetch updated settings after successful recovery
|
||||
try {
|
||||
const settingsResponse = await fetch('/api/v2/device_settings')
|
||||
const settingsData = await settingsResponse.json()
|
||||
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
playerName: settingsData.player_name || '',
|
||||
defaultDuration: settingsData.default_duration || 0,
|
||||
defaultStreamingDuration:
|
||||
settingsData.default_streaming_duration || 0,
|
||||
audioOutput: settingsData.audio_output || 'hdmi',
|
||||
dateFormat: settingsData.date_format || 'mm/dd/yyyy',
|
||||
authBackend: settingsData.auth_backend || '',
|
||||
user: settingsData.username || '',
|
||||
showSplash: settingsData.show_splash || false,
|
||||
defaultAssets: settingsData.default_assets || false,
|
||||
shufflePlaylist: settingsData.shuffle_playlist || false,
|
||||
use24HourClock: settingsData.use_24_hour_clock || false,
|
||||
debugLogging: settingsData.debug_logging || false,
|
||||
}))
|
||||
setPrevAuthBackend(settingsData.auth_backend || '')
|
||||
} catch (settingsErr) {}
|
||||
}
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
title: 'Error!',
|
||||
text:
|
||||
err.message ||
|
||||
'The operation failed. Please reload the page and try again.',
|
||||
icon: 'error',
|
||||
confirmButtonColor: '#dc3545',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
document.querySelector('.progress').style.display = 'none'
|
||||
document.getElementById('btn-upload').style.display = 'inline-block'
|
||||
document.getElementById('btn-backup').style.display = 'inline-block'
|
||||
// Reset the file input
|
||||
e.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleShutdown = async () => {
|
||||
const result = await Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: 'Are you sure you want to shutdown your device?',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Shutdown',
|
||||
cancelButtonText: 'Cancel',
|
||||
reverseButtons: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonColor: '#6c757d',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
cancelButton: 'swal2-cancel',
|
||||
actions: 'swal2-actions',
|
||||
},
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await fetch('/api/v2/shutdown', {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to shutdown device')
|
||||
}
|
||||
|
||||
await Swal.fire({
|
||||
title: 'Success!',
|
||||
text: 'Device shutdown has started successfully.\nSoon you will be able to unplug the power from your Raspberry Pi.',
|
||||
icon: 'success',
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
title: 'Error!',
|
||||
text:
|
||||
err.message ||
|
||||
'The operation failed. Please reload the page and try again.',
|
||||
icon: 'error',
|
||||
confirmButtonColor: '#dc3545',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleReboot = async () => {
|
||||
const result = await Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: 'Are you sure you want to reboot your device?',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Reboot',
|
||||
cancelButtonText: 'Cancel',
|
||||
reverseButtons: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonColor: '#6c757d',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
cancelButton: 'swal2-cancel',
|
||||
actions: 'swal2-actions',
|
||||
},
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await fetch('/api/v2/reboot', {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to reboot device')
|
||||
}
|
||||
|
||||
await Swal.fire({
|
||||
title: 'Success!',
|
||||
text: 'Reboot has started successfully.',
|
||||
icon: 'success',
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
title: 'Error!',
|
||||
text:
|
||||
err.message ||
|
||||
'The operation failed. Please reload the page and try again.',
|
||||
icon: 'error',
|
||||
confirmButtonColor: '#dc3545',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Settings'
|
||||
// Load initial settings
|
||||
fetch('/api/v2/device_settings')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
playerName: data.player_name || '',
|
||||
defaultDuration: data.default_duration || 0,
|
||||
defaultStreamingDuration: data.default_streaming_duration || 0,
|
||||
audioOutput: data.audio_output || 'hdmi',
|
||||
dateFormat: data.date_format || 'mm/dd/yyyy',
|
||||
authBackend: data.auth_backend || '',
|
||||
user: data.username || '',
|
||||
showSplash: data.show_splash || false,
|
||||
defaultAssets: data.default_assets || false,
|
||||
shufflePlaylist: data.shuffle_playlist || false,
|
||||
use24HourClock: data.use_24_hour_clock || false,
|
||||
debugLogging: data.debug_logging || false,
|
||||
}))
|
||||
setPrevAuthBackend(data.auth_backend || '')
|
||||
})
|
||||
.catch(() => {
|
||||
setError('Failed to load settings. Please try again.')
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value, type, checked } = e.target
|
||||
if (name === 'authBackend') {
|
||||
setPrevAuthBackend(settings.authBackend)
|
||||
}
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
[name]: type === 'checkbox' ? checked : value,
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v2/device_settings', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
player_name: settings.playerName,
|
||||
default_duration: settings.defaultDuration,
|
||||
default_streaming_duration: settings.defaultStreamingDuration,
|
||||
audio_output: settings.audioOutput,
|
||||
date_format: settings.dateFormat,
|
||||
auth_backend: settings.authBackend,
|
||||
current_password: settings.currentPassword,
|
||||
username: settings.user,
|
||||
password: settings.password,
|
||||
password_2: settings.confirmPassword,
|
||||
show_splash: settings.showSplash,
|
||||
default_assets: settings.defaultAssets,
|
||||
shuffle_playlist: settings.shufflePlaylist,
|
||||
use_24_hour_clock: settings.use24HourClock,
|
||||
debug_logging: settings.debugLogging,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to save settings')
|
||||
}
|
||||
|
||||
await Swal.fire({
|
||||
title: 'Success!',
|
||||
text: 'Settings were successfully saved.',
|
||||
icon: 'success',
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
},
|
||||
})
|
||||
|
||||
// Clear password after successful save
|
||||
setSettings((prev) => ({ ...prev, currentPassword: '' }))
|
||||
// Fetch updated device settings
|
||||
dispatch(fetchDeviceSettings())
|
||||
// Reset the form
|
||||
e.target.reset()
|
||||
} catch (err) {
|
||||
await Swal.fire({
|
||||
title: 'Error!',
|
||||
text: err.message || 'Failed to save settings',
|
||||
icon: 'error',
|
||||
confirmButtonColor: '#dc3545',
|
||||
customClass: {
|
||||
popup: 'swal2-popup',
|
||||
title: 'swal2-title',
|
||||
htmlContainer: 'swal2-html-container',
|
||||
confirmButton: 'swal2-confirm',
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row py-2">
|
||||
<div className="col-12">
|
||||
<h4 className="page-header text-white">
|
||||
<b>Settings</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row content px-3">
|
||||
<div className="col-12 my-3">
|
||||
<form onSubmit={handleSubmit} className="row">
|
||||
<div className="form-group col-6 d-flex flex-column justify-content-between">
|
||||
<div className="form-group">
|
||||
<label className="small text-secondary">
|
||||
<small>Player name</small>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
name="playerName"
|
||||
type="text"
|
||||
value={settings.playerName}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="form-group col-6">
|
||||
<label className="small text-secondary">
|
||||
<small>Default duration (seconds)</small>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
name="defaultDuration"
|
||||
type="number"
|
||||
value={settings.defaultDuration}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-6">
|
||||
<label className="small text-secondary">
|
||||
<small>Default streaming duration (seconds)</small>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
name="defaultStreamingDuration"
|
||||
type="number"
|
||||
value={settings.defaultStreamingDuration}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="small text-secondary">
|
||||
<small>Audio output</small>
|
||||
</label>
|
||||
<select
|
||||
className="form-control"
|
||||
name="audioOutput"
|
||||
value={settings.audioOutput}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="hdmi">HDMI</option>
|
||||
<option value="local">3.5mm jack</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="small text-secondary">
|
||||
<small>Date format</small>
|
||||
</label>
|
||||
<select
|
||||
className="form-control"
|
||||
name="dateFormat"
|
||||
value={settings.dateFormat}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="mm/dd/yyyy">month/day/year</option>
|
||||
<option value="dd/mm/yyyy">day/month/year</option>
|
||||
<option value="yyyy/mm/dd">year/month/day</option>
|
||||
<option value="mm-dd-yyyy">month-day-year</option>
|
||||
<option value="dd-mm-yyyy">day-month-year</option>
|
||||
<option value="yyyy-mm-dd">year-month-day</option>
|
||||
<option value="mm.dd.yyyy">month.day.year</option>
|
||||
<option value="dd.mm.yyyy">day.month.year</option>
|
||||
<option value="yyyy.mm.dd">year.month.day</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-0">
|
||||
<label className="small text-secondary">
|
||||
<small>Authentication</small>
|
||||
</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="auth_backend"
|
||||
name="authBackend"
|
||||
value={settings.authBackend}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">Disabled</option>
|
||||
<option value="auth_basic">Basic</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{(settings.authBackend === 'auth_basic' ||
|
||||
(settings.authBackend === '' &&
|
||||
prevAuthBackend === 'auth_basic')) && (
|
||||
<>
|
||||
{prevAuthBackend === 'auth_basic' && (
|
||||
<div className="form-group" id="curpassword_group">
|
||||
<label className="small text-secondary">
|
||||
<small>Current Password</small>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
name="currentPassword"
|
||||
type="password"
|
||||
value={settings.currentPassword}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{settings.authBackend === 'auth_basic' && (
|
||||
<>
|
||||
<div className="form-group" id="user_group">
|
||||
<label className="small text-secondary">
|
||||
<small>User</small>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
name="user"
|
||||
type="text"
|
||||
value={settings.user}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="form-group col-6" id="password_group">
|
||||
<label className="small text-secondary">
|
||||
<small>Password</small>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
name="password"
|
||||
type="password"
|
||||
value={settings.password}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-6" id="password2_group">
|
||||
<label className="small text-secondary">
|
||||
<small>Confirm Password</small>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
value={settings.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group col-6 d-flex flex-column justify-content-start">
|
||||
<div className="form-inline mt-4">
|
||||
<label>Show splash screen</label>
|
||||
<div className="ml-auto">
|
||||
<label className="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
<input
|
||||
name="showSplash"
|
||||
type="checkbox"
|
||||
checked={settings.showSplash}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<a></a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-inline mt-4">
|
||||
<label>Default assets</label>
|
||||
<div className="ml-auto">
|
||||
<label className="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
<input
|
||||
name="defaultAssets"
|
||||
type="checkbox"
|
||||
checked={settings.defaultAssets}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<a></a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-inline mt-4">
|
||||
<label>Shuffle playlist</label>
|
||||
<div className="ml-auto">
|
||||
<label className="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
<input
|
||||
name="shufflePlaylist"
|
||||
type="checkbox"
|
||||
checked={settings.shufflePlaylist}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<a></a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-inline mt-4">
|
||||
<label>Use 24-hour clock</label>
|
||||
<div className="ml-auto">
|
||||
<label className="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
<input
|
||||
name="use24HourClock"
|
||||
type="checkbox"
|
||||
checked={settings.use24HourClock}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<a></a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-inline mt-4">
|
||||
<label>Debug logging</label>
|
||||
<div className="ml-auto">
|
||||
<label className="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
<input
|
||||
name="debugLogging"
|
||||
type="checkbox"
|
||||
checked={settings.debugLogging}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<a></a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-12">
|
||||
<div className="text-right">
|
||||
<a className="btn btn-long btn-outline-primary mr-2" href="/">
|
||||
Cancel
|
||||
</a>
|
||||
<button
|
||||
className="btn btn-long btn-primary"
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Saving...' : 'Save Settings'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backup Section */}
|
||||
<div className="row py-2 mt-4">
|
||||
<div className="col-12">
|
||||
<h4 className="page-header text-white">
|
||||
<b>Backup</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row content px-3">
|
||||
<div id="backup-section" className="col-12 my-3">
|
||||
<div className="text-right">
|
||||
<input
|
||||
name="backup_upload"
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<button
|
||||
id="btn-backup"
|
||||
className="btn btn-long btn-outline-primary mr-2"
|
||||
onClick={handleBackup}
|
||||
disabled={isUploading}
|
||||
>
|
||||
Get Backup
|
||||
</button>
|
||||
<button
|
||||
id="btn-upload"
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
onClick={handleUpload}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading ? 'Uploading...' : 'Upload and Recover'}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="progress-bar progress-bar-striped progress active w-100"
|
||||
style={{ display: isUploading ? 'block' : 'none' }}
|
||||
>
|
||||
<div className="bar" style={{ width: `${uploadProgress}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Controls Section */}
|
||||
<div className="row py-2 mt-4">
|
||||
<div className="col-12">
|
||||
<h4 className="page-header text-white">
|
||||
<b>System Controls</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row content px-3">
|
||||
<div className="col-12 my-3">
|
||||
<div className="text-right">
|
||||
<button
|
||||
className="btn btn-danger btn-long mr-2"
|
||||
type="button"
|
||||
onClick={handleReboot}
|
||||
>
|
||||
Reboot
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-long mr-2"
|
||||
type="button"
|
||||
onClick={handleShutdown}
|
||||
>
|
||||
Shutdown
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
static/src/components/sortable-asset-row.jsx
Normal file
33
static/src/components/sortable-asset-row.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { AssetRow } from './asset-row'
|
||||
|
||||
export const SortableAssetRow = (props) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: props.id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.95 : 1,
|
||||
zIndex: isDragging ? 99999 : 'auto',
|
||||
position: isDragging ? 'relative' : 'static',
|
||||
backgroundColor: isDragging ? 'rgba(255, 255, 255, 0.1)' : 'transparent',
|
||||
}
|
||||
|
||||
return (
|
||||
<AssetRow
|
||||
{...props}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
dragHandleProps={{ ...attributes, ...listeners }}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
)
|
||||
}
|
||||
188
static/src/components/system-info.jsx
Normal file
188
static/src/components/system-info.jsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const ANTHIAS_REPO_URL = 'https://github.com/Screenly/Anthias'
|
||||
|
||||
const AnthiasVersionValue = ({ version }) => {
|
||||
const [commitLink, setCommitLink] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!version) {
|
||||
return
|
||||
}
|
||||
|
||||
const [gitBranch, gitCommit] = version ? version.split('@') : ['', '']
|
||||
|
||||
if (gitBranch === 'master') {
|
||||
setCommitLink(`${ANTHIAS_REPO_URL}/commit/${gitCommit}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (commitLink) {
|
||||
return (
|
||||
<a href={commitLink} rel="noopener" target="_blank" class="text-dark">
|
||||
{version}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{version}</>
|
||||
}
|
||||
|
||||
const Skeleton = ({ children, isLoading }) => {
|
||||
return isLoading ? (
|
||||
<span className="placeholder placeholder-wave"></span>
|
||||
) : (
|
||||
children
|
||||
)
|
||||
}
|
||||
|
||||
export const SystemInfo = () => {
|
||||
const [loadAverage, setLoadAverage] = useState('')
|
||||
const [freeSpace, setFreeSpace] = useState('')
|
||||
const [memory, setMemory] = useState({})
|
||||
const [uptime, setUptime] = useState({})
|
||||
const [displayPower, setDisplayPower] = useState(null)
|
||||
const [deviceModel, setDeviceModel] = useState('')
|
||||
const [anthiasVersion, setAnthiasVersion] = useState('')
|
||||
const [macAddress, setMacAddress] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const initializeSystemInfo = async () => {
|
||||
setIsLoading(true)
|
||||
|
||||
const response = await fetch('/api/v2/info', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
setIsLoading(false)
|
||||
throw new Error('Failed to fetch system info')
|
||||
}
|
||||
|
||||
const systemInfo = await response.json()
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
setLoadAverage(systemInfo.loadavg)
|
||||
setFreeSpace(systemInfo.free_space)
|
||||
setMemory(systemInfo.memory)
|
||||
setUptime(systemInfo.uptime)
|
||||
setDisplayPower(systemInfo.display_power)
|
||||
setDeviceModel(systemInfo.device_model)
|
||||
setAnthiasVersion(systemInfo.anthias_version)
|
||||
setMacAddress(systemInfo.mac_address)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'System Info'
|
||||
initializeSystemInfo()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row py-2">
|
||||
<div className="col-12">
|
||||
<h4 className="page-header text-white">
|
||||
<b>System Info</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row content">
|
||||
<div className="col-12">
|
||||
<table className="table mb-5">
|
||||
<thead className="table-borderless">
|
||||
<tr>
|
||||
<th
|
||||
className="text-secondary font-weight-normal"
|
||||
scope="col"
|
||||
style={{ width: '20%' }}
|
||||
>
|
||||
Option
|
||||
</th>
|
||||
<th className="text-secondary font-weight-normal" scope="col">
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Load Average</th>
|
||||
<td>
|
||||
<Skeleton isLoading={isLoading}>{loadAverage}</Skeleton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Free Space</th>
|
||||
<td>
|
||||
<Skeleton isLoading={isLoading}>{freeSpace}</Skeleton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Memory</th>
|
||||
<td>
|
||||
<Skeleton isLoading={isLoading}>
|
||||
<div>
|
||||
Total: <strong>{memory.total}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Used: <strong>{memory.used}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Free: <strong>{memory.free}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Shared: <strong>{memory.shared}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Buff: <strong>{memory.buff}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Available: <strong>{memory.available}</strong>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Uptime</th>
|
||||
<td>
|
||||
<Skeleton isLoading={isLoading}>
|
||||
{uptime.days} days and {uptime.hours} hours
|
||||
</Skeleton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Display Power (CEC)</th>
|
||||
<td>
|
||||
<Skeleton isLoading={isLoading}>
|
||||
{displayPower || 'None'}
|
||||
</Skeleton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Device Model</th>
|
||||
<td>
|
||||
<Skeleton isLoading={isLoading}>{deviceModel}</Skeleton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Anthias Version</th>
|
||||
<td>
|
||||
<Skeleton isLoading={isLoading}>
|
||||
<AnthiasVersionValue version={anthiasVersion} />
|
||||
</Skeleton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MAC Address</th>
|
||||
<td>
|
||||
<Skeleton isLoading={isLoading}>{macAddress}</Skeleton>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
static/src/index.js
Normal file
17
static/src/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router'
|
||||
import { Provider } from 'react-redux'
|
||||
import { store } from './store'
|
||||
|
||||
import '@/sass/anthias.scss'
|
||||
import { App } from '@/components/app'
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('app'))
|
||||
|
||||
root.render(
|
||||
<BrowserRouter basename="/">
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</BrowserRouter>,
|
||||
)
|
||||
335
static/src/store/assets/asset-modal-slice.js
Normal file
335
static/src/store/assets/asset-modal-slice.js
Normal file
@@ -0,0 +1,335 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { addAsset } from './assets-list-slice'
|
||||
|
||||
// Async thunks for API operations
|
||||
export const uploadFile = createAsyncThunk(
|
||||
'assetModal/uploadFile',
|
||||
async ({ file, skipAssetCheck }, { dispatch, getState, rejectWithValue }) => {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file_upload', file)
|
||||
|
||||
// Create XMLHttpRequest for progress tracking
|
||||
const xhr = new XMLHttpRequest()
|
||||
|
||||
const uploadPromise = new Promise((resolve, reject) => {
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const progress = Math.round((e.loaded / e.total) * 100)
|
||||
dispatch(setUploadProgress(progress))
|
||||
}
|
||||
})
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText)
|
||||
resolve(response)
|
||||
} catch (error) {
|
||||
reject(new Error('Invalid JSON response'))
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`))
|
||||
}
|
||||
})
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('Network error during upload'))
|
||||
})
|
||||
|
||||
xhr.addEventListener('abort', () => {
|
||||
reject(new Error('Upload aborted'))
|
||||
})
|
||||
})
|
||||
|
||||
// Start the upload
|
||||
xhr.open('POST', '/api/v2/file_asset')
|
||||
xhr.send(formData)
|
||||
|
||||
// Wait for upload to complete
|
||||
const response = await uploadPromise
|
||||
|
||||
// Get mimetype and duration
|
||||
const mimetype = getMimetype(file.name)
|
||||
const state = getState()
|
||||
const duration = getDurationForMimetype(
|
||||
mimetype,
|
||||
state.assetModal.defaultDuration,
|
||||
state.assetModal.defaultStreamingDuration,
|
||||
)
|
||||
const dates = getDefaultDates()
|
||||
|
||||
return {
|
||||
fileData: response,
|
||||
filename: file.name,
|
||||
skipAssetCheck,
|
||||
mimetype,
|
||||
duration,
|
||||
dates,
|
||||
}
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const saveAsset = createAsyncThunk(
|
||||
'assetModal/saveAsset',
|
||||
async ({ assetData }, { dispatch, rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch('/api/v2/assets', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(assetData),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return rejectWithValue('Failed to save asset')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Create the complete asset object with the response data
|
||||
const completeAsset = {
|
||||
...assetData,
|
||||
asset_id: data.asset_id,
|
||||
...data,
|
||||
}
|
||||
|
||||
// Dispatch the addAsset action to update the assets list
|
||||
dispatch(addAsset(completeAsset))
|
||||
|
||||
return completeAsset
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const fetchDeviceSettings = createAsyncThunk(
|
||||
'assetModal/fetchDeviceSettings',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch('/api/v2/device_settings')
|
||||
if (!response.ok) {
|
||||
return rejectWithValue('Failed to fetch device settings')
|
||||
}
|
||||
const data = await response.json()
|
||||
return {
|
||||
defaultDuration: data.default_duration,
|
||||
defaultStreamingDuration: data.default_streaming_duration,
|
||||
}
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Helper functions
|
||||
const getMimetype = (filename) => {
|
||||
const viduris = ['rtsp', 'rtmp']
|
||||
const mimetypes = [
|
||||
[['jpe', 'jpg', 'jpeg', 'png', 'pnm', 'gif', 'bmp'], 'image'],
|
||||
[['avi', 'mkv', 'mov', 'mpg', 'mpeg', 'mp4', 'ts', 'flv'], 'video'],
|
||||
]
|
||||
const domains = [[['www.youtube.com', 'youtu.be'], 'youtube_asset']]
|
||||
|
||||
// Check if it's a streaming URL
|
||||
const scheme = filename.split(':')[0].toLowerCase()
|
||||
if (viduris.includes(scheme)) {
|
||||
return 'streaming'
|
||||
}
|
||||
|
||||
// Check if it's a domain-specific asset
|
||||
try {
|
||||
const domain = filename.split('//')[1].toLowerCase().split('/')[0]
|
||||
for (const [domainList, type] of domains) {
|
||||
if (domainList.includes(domain)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid URL format
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
try {
|
||||
const ext = filename.split('.').pop().toLowerCase()
|
||||
for (const [extList, type] of mimetypes) {
|
||||
if (extList.includes(ext)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// No extension found
|
||||
}
|
||||
|
||||
// Default to webpage
|
||||
return 'webpage'
|
||||
}
|
||||
|
||||
const getDurationForMimetype = (
|
||||
mimetype,
|
||||
defaultDuration,
|
||||
defaultStreamingDuration,
|
||||
) => {
|
||||
if (mimetype === 'video') {
|
||||
return 0
|
||||
} else if (mimetype === 'streaming') {
|
||||
return defaultStreamingDuration
|
||||
} else {
|
||||
return defaultDuration
|
||||
}
|
||||
}
|
||||
|
||||
const getDefaultDates = () => {
|
||||
const now = new Date()
|
||||
const endDate = new Date()
|
||||
endDate.setDate(endDate.getDate() + 30) // 30 days from now
|
||||
|
||||
return {
|
||||
start_date: now.toISOString(),
|
||||
end_date: endDate.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
// Slice definition
|
||||
const assetModalSlice = createSlice({
|
||||
name: 'assetModal',
|
||||
initialState: {
|
||||
activeTab: 'uri',
|
||||
formData: {
|
||||
uri: '',
|
||||
skipAssetCheck: false,
|
||||
},
|
||||
isValid: true,
|
||||
errorMessage: '',
|
||||
statusMessage: '',
|
||||
isSubmitting: false,
|
||||
uploadProgress: 0,
|
||||
defaultDuration: 10, // Default fallback value
|
||||
defaultStreamingDuration: 300, // Default fallback value
|
||||
},
|
||||
reducers: {
|
||||
setActiveTab: (state, action) => {
|
||||
state.activeTab = action.payload
|
||||
},
|
||||
updateFormData: (state, action) => {
|
||||
state.formData = { ...state.formData, ...action.payload }
|
||||
},
|
||||
setValid: (state, action) => {
|
||||
state.isValid = action.payload
|
||||
},
|
||||
setErrorMessage: (state, action) => {
|
||||
state.errorMessage = action.payload
|
||||
},
|
||||
setStatusMessage: (state, action) => {
|
||||
state.statusMessage = action.payload
|
||||
},
|
||||
setUploadProgress: (state, action) => {
|
||||
state.uploadProgress = action.payload
|
||||
},
|
||||
resetForm: (state) => {
|
||||
state.formData = {
|
||||
uri: '',
|
||||
skipAssetCheck: false,
|
||||
}
|
||||
state.isValid = true
|
||||
state.errorMessage = ''
|
||||
state.statusMessage = ''
|
||||
state.isSubmitting = false
|
||||
state.uploadProgress = 0
|
||||
},
|
||||
validateUrl: (state, action) => {
|
||||
const url = action.payload
|
||||
if (!url) {
|
||||
state.isValid = true
|
||||
state.errorMessage = ''
|
||||
return
|
||||
}
|
||||
|
||||
const urlPattern =
|
||||
/(http|https|rtsp|rtmp):\/\/[\w-]+(\.?[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/
|
||||
const isValidUrl = urlPattern.test(url)
|
||||
|
||||
state.isValid = isValidUrl
|
||||
state.errorMessage = isValidUrl ? '' : 'Please enter a valid URL'
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// Device settings
|
||||
.addCase(fetchDeviceSettings.fulfilled, (state, action) => {
|
||||
state.defaultDuration = action.payload.defaultDuration
|
||||
state.defaultStreamingDuration = action.payload.defaultStreamingDuration
|
||||
})
|
||||
// Upload file
|
||||
.addCase(uploadFile.pending, (state) => {
|
||||
state.isSubmitting = true
|
||||
state.statusMessage = ''
|
||||
state.uploadProgress = 0
|
||||
})
|
||||
.addCase(uploadFile.fulfilled, (state, action) => {
|
||||
const {
|
||||
fileData,
|
||||
filename,
|
||||
skipAssetCheck,
|
||||
mimetype,
|
||||
duration,
|
||||
dates,
|
||||
} = action.payload
|
||||
|
||||
// Update form data with file name and other details
|
||||
state.formData = {
|
||||
...state.formData,
|
||||
name: filename,
|
||||
uri: fileData.uri,
|
||||
skipAssetCheck,
|
||||
mimetype,
|
||||
duration,
|
||||
dates,
|
||||
}
|
||||
|
||||
state.statusMessage = 'Upload completed.'
|
||||
state.isSubmitting = false
|
||||
state.uploadProgress = 0
|
||||
})
|
||||
.addCase(uploadFile.rejected, (state, action) => {
|
||||
state.errorMessage = `Upload failed: ${action.payload}`
|
||||
state.isSubmitting = false
|
||||
state.uploadProgress = 0
|
||||
})
|
||||
// Save asset
|
||||
.addCase(saveAsset.pending, (state) => {
|
||||
state.isSubmitting = true
|
||||
})
|
||||
.addCase(saveAsset.fulfilled, (state) => {
|
||||
state.isSubmitting = false
|
||||
state.statusMessage = 'Asset saved successfully.'
|
||||
})
|
||||
.addCase(saveAsset.rejected, (state, action) => {
|
||||
state.errorMessage = `Failed to save asset: ${action.payload}`
|
||||
state.isSubmitting = false
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Export actions
|
||||
export const {
|
||||
setActiveTab,
|
||||
updateFormData,
|
||||
setValid,
|
||||
setErrorMessage,
|
||||
setStatusMessage,
|
||||
setUploadProgress,
|
||||
resetForm,
|
||||
validateUrl,
|
||||
} = assetModalSlice.actions
|
||||
|
||||
// Export selectors
|
||||
export const selectAssetModalState = (state) => state.assetModal
|
||||
|
||||
// Export reducer
|
||||
export default assetModalSlice.reducer
|
||||
49
static/src/store/assets/assets-list-slice.js
Normal file
49
static/src/store/assets/assets-list-slice.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import {
|
||||
fetchAssets,
|
||||
updateAssetOrder,
|
||||
toggleAssetEnabled,
|
||||
} from '@/store/assets/assets-thunks'
|
||||
|
||||
const assetsSlice = createSlice({
|
||||
name: 'assets',
|
||||
initialState: {
|
||||
items: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
},
|
||||
reducers: {
|
||||
addAsset: (state, action) => {
|
||||
state.items.push(action.payload)
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchAssets.pending, (state) => {
|
||||
state.status = 'loading'
|
||||
})
|
||||
.addCase(fetchAssets.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded'
|
||||
state.items = action.payload
|
||||
})
|
||||
.addCase(fetchAssets.rejected, (state, action) => {
|
||||
state.status = 'failed'
|
||||
state.error = action.error.message
|
||||
})
|
||||
.addCase(updateAssetOrder.fulfilled, (state) => {
|
||||
state.status = 'succeeded'
|
||||
})
|
||||
.addCase(toggleAssetEnabled.fulfilled, (state, action) => {
|
||||
const { assetId, newValue, playOrder } = action.payload
|
||||
const asset = state.items.find((item) => item.asset_id === assetId)
|
||||
if (asset) {
|
||||
asset.is_enabled = newValue
|
||||
asset.play_order = playOrder
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { addAsset } = assetsSlice.actions
|
||||
|
||||
export default assetsSlice.reducer
|
||||
7
static/src/store/assets/assets-selectors.js
Normal file
7
static/src/store/assets/assets-selectors.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const selectActiveAssets = (state) =>
|
||||
state.assets.items
|
||||
.filter((asset) => asset.is_active)
|
||||
.sort((a, b) => a.play_order - b.play_order)
|
||||
|
||||
export const selectInactiveAssets = (state) =>
|
||||
state.assets.items.filter((asset) => !asset.is_active)
|
||||
66
static/src/store/assets/assets-thunks.js
Normal file
66
static/src/store/assets/assets-thunks.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit'
|
||||
|
||||
export const fetchAssets = createAsyncThunk('assets/fetchAssets', async () => {
|
||||
const response = await fetch('/api/v2/assets')
|
||||
const data = await response.json()
|
||||
return data
|
||||
})
|
||||
|
||||
export const updateAssetOrder = createAsyncThunk(
|
||||
'assets/updateOrder',
|
||||
async (orderedIds) => {
|
||||
const response = await fetch('/api/v2/assets/order', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ids: orderedIds }),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update order')
|
||||
}
|
||||
return orderedIds
|
||||
},
|
||||
)
|
||||
|
||||
export const toggleAssetEnabled = createAsyncThunk(
|
||||
'assets/toggleEnabled',
|
||||
async ({ assetId, newValue }, { dispatch, getState }) => {
|
||||
// First, fetch the current assets to determine the next play_order
|
||||
const response = await fetch('/api/v2/assets')
|
||||
const assets = await response.json()
|
||||
|
||||
// Get the current active assets to determine the next play_order
|
||||
const activeAssets = assets.filter((asset) => asset.is_active)
|
||||
|
||||
// If enabling the asset, set play_order to the next available position
|
||||
// If disabling the asset, set play_order to 0
|
||||
const playOrder = newValue === 1 ? activeAssets.length : 0
|
||||
|
||||
const updateResponse = await fetch(`/api/v2/assets/${assetId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
is_enabled: newValue,
|
||||
play_order: playOrder,
|
||||
}),
|
||||
})
|
||||
|
||||
const activeAssetIds = getState()
|
||||
.assets.items.filter((asset) => asset.is_active)
|
||||
.sort((a, b) => a.play_order - b.play_order)
|
||||
.map((asset) => asset.asset_id)
|
||||
.concat(assetId)
|
||||
|
||||
await dispatch(updateAssetOrder(activeAssetIds.join(',')))
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
throw new Error('Failed to update asset')
|
||||
}
|
||||
|
||||
// Return both the assetId and newValue for the reducer
|
||||
return { assetId, newValue, playOrder }
|
||||
},
|
||||
)
|
||||
48
static/src/store/assets/index.js
Normal file
48
static/src/store/assets/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import assetsReducer from '@/store/assets/assets-list-slice'
|
||||
import { addAsset } from '@/store/assets/assets-list-slice'
|
||||
import {
|
||||
fetchAssets,
|
||||
updateAssetOrder,
|
||||
toggleAssetEnabled,
|
||||
} from '@/store/assets/assets-thunks'
|
||||
import {
|
||||
selectActiveAssets,
|
||||
selectInactiveAssets,
|
||||
} from '@/store/assets/assets-selectors'
|
||||
import assetModalReducer from './asset-modal-slice'
|
||||
import {
|
||||
uploadFile,
|
||||
saveAsset,
|
||||
setActiveTab,
|
||||
updateFormData,
|
||||
setValid,
|
||||
setErrorMessage,
|
||||
setStatusMessage,
|
||||
setUploadProgress,
|
||||
resetForm,
|
||||
validateUrl,
|
||||
selectAssetModalState,
|
||||
} from './asset-modal-slice'
|
||||
|
||||
export {
|
||||
assetsReducer,
|
||||
addAsset,
|
||||
fetchAssets,
|
||||
updateAssetOrder,
|
||||
toggleAssetEnabled,
|
||||
selectActiveAssets,
|
||||
selectInactiveAssets,
|
||||
// Asset Modal exports
|
||||
assetModalReducer,
|
||||
uploadFile,
|
||||
saveAsset,
|
||||
setActiveTab,
|
||||
updateFormData,
|
||||
setValid,
|
||||
setErrorMessage,
|
||||
setStatusMessage,
|
||||
setUploadProgress,
|
||||
resetForm,
|
||||
validateUrl,
|
||||
selectAssetModalState,
|
||||
}
|
||||
9
static/src/store/index.js
Normal file
9
static/src/store/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { assetsReducer, assetModalReducer } from '@/store/assets'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
assets: assetsReducer,
|
||||
assetModal: assetModalReducer,
|
||||
},
|
||||
})
|
||||
11
static/src/utils.js
Normal file
11
static/src/utils.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* CSS tagged template literal helper function
|
||||
* @param {TemplateStringsArray} strings - The template literal strings
|
||||
* @param {...any} values - The template literal values
|
||||
* @returns {string} The processed CSS string
|
||||
*/
|
||||
export const css = (strings, ...values) => {
|
||||
return strings.reduce((result, string, i) => {
|
||||
return result + string + (values[i] || '')
|
||||
}, '')
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="form-group" id="user_group">
|
||||
<label class="small text-secondary">User</label>
|
||||
<input class="form-control" name="user" type="text" value="{{ user }}">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6" id="password_group">
|
||||
<label class="small text-secondary">Password</label>
|
||||
<input class="form-control" name="password" type="password" value="">
|
||||
</div>
|
||||
<div class="form-group col-6" id="password2_group">
|
||||
<label class="small text-secondary">Confirm Password</label>
|
||||
<input class="form-control" name="password2" type="password" value="">
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,47 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% include "head.html" %}
|
||||
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% include 'header.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-6 small text-white">
|
||||
<span>Want to get more out of your digital signage?
|
||||
<a class="brand"
|
||||
href="https://www.screenly.io/?utm_source=Anthias&utm_medium=root-page&utm_campaign=UI"
|
||||
target="_blank">
|
||||
<strong>Try Screenly</strong>.
|
||||
</a></span>
|
||||
</div>
|
||||
<div class="col-6 text-right small">
|
||||
<span>
|
||||
<a href="javascript://" id="subsribe-form-container" data-html="true" data-trigger="focus"
|
||||
data-placement="top">
|
||||
<strong>Get the latest Screenly news directly in your mailbox.</strong>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
<script type="text/javascript">
|
||||
$(".navbar .nav li a").removeClass('active');
|
||||
$(".navbar .nav li a[href='" + window.location.pathname + "']").addClass('active');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,35 +0,0 @@
|
||||
<footer id="footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div id="screenly-logo" class="col-12 row m-2 ml-0 mr-0">
|
||||
<div class="links offset-3 col-6 text-center justify-content-center align-self-center">
|
||||
<a href="/api/docs/" target="_blank" class="mr-4 small">
|
||||
API
|
||||
</a>
|
||||
<a href="https://anthias.screenly.io/#faq?utm_source=Anthias&utm_medium=footer&utm_campaign=UI" target="_blank" class="mr-4 small">
|
||||
FAQ
|
||||
</a>
|
||||
<a href="http://screenly.io/?utm_source=Anthias&utm_medium=footer&utm_campaign=UI" target="_blank" class="mr-4 small">
|
||||
Screenly.io
|
||||
</a>
|
||||
<a href="https://forums.screenly.io/" target="_blank" class="mr-4 small">
|
||||
Support
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="github-stars" class="col-3 text-right justify-content-center align-self-center">
|
||||
<a class="github-button" href="https://github.com/screenly/anthias" data-size="large" data-show-count="true" aria-label="Star screenly/anthias on GitHub">
|
||||
Star
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy pb-4">
|
||||
<div class="container">
|
||||
<div class="text-center p-2">
|
||||
© Screenly, Inc.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,57 +0,0 @@
|
||||
{# vim: ft=htmldjango #}
|
||||
{% load static %}
|
||||
|
||||
<meta charset="utf-8"/>
|
||||
|
||||
{% if context.player_name %}
|
||||
<title>Anthias - {{ context.player_name }}</title>
|
||||
{% else %}
|
||||
<title>Anthias</title>
|
||||
{% endif %}
|
||||
|
||||
<link href="/static/favicons/apple-touch-icon-57x57.png" rel="apple-touch-icon-precomposed" sizes="57x57"/>
|
||||
<link href="/static/favicons/apple-touch-icon-114x114.png" rel="apple-touch-icon-precomposed" sizes="114x114"/>
|
||||
<link href="/static/favicons/apple-touch-icon-72x72.png" rel="apple-touch-icon-precomposed" sizes="72x72"/>
|
||||
<link href="/static/favicons/apple-touch-icon-144x144.png" rel="apple-touch-icon-precomposed" sizes="144x144"/>
|
||||
<link href="/static/favicons/apple-touch-icon-60x60.png" rel="apple-touch-icon-precomposed" sizes="60x60"/>
|
||||
<link href="/static/favicons/apple-touch-icon-120x120.png" rel="apple-touch-icon-precomposed" sizes="120x120"/>
|
||||
<link href="/static/favicons/apple-touch-icon-76x76.png" rel="apple-touch-icon-precomposed" sizes="76x76"/>
|
||||
<link href="/static/favicons/apple-touch-icon-152x152.png" rel="apple-touch-icon-precomposed" sizes="152x152"/>
|
||||
<link href="/static/favicons/favicon-196x196.png" rel="icon" sizes="196x196" type="image/png"/>
|
||||
<link href="/static/favicons/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/>
|
||||
<link href="/static/favicons/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/>
|
||||
<link href="/static/favicons/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/>
|
||||
<link href="/static/favicons/favicon-128.png" rel="icon" sizes="128x128" type="image/png"/>
|
||||
<link href="https://fonts.googleapis.com/css?family=Plus Jakarta Sans" rel="stylesheet">
|
||||
<link rel="icon" href="/static/favicons/favicon.ico">
|
||||
<meta content="anthias;" name="application-name"/>
|
||||
<meta content="#FFFFFF" name="msapplication-TileColor"/>
|
||||
<meta content="/static/favicons/mstile-144x144.png" name="msapplication-TileImage"/>
|
||||
<meta content="/static/favicons/mstile-70x70.png" name="msapplication-square70x70logo"/>
|
||||
<meta content="/static/favicons/mstile-150x150.png" name="msapplication-square150x150logo"/>
|
||||
<meta content="/static/favicons/mstile-310x150.png" name="msapplication-wide310x150logo"/>
|
||||
<meta content="/static/favicons/mstile-310x310.png" name="msapplication-square310x310logo"/>
|
||||
|
||||
<link href="{% static 'dist/css/anthias.css' %}" type="text/css" rel="stylesheet"/>
|
||||
|
||||
<link href="{% static 'fontawesome/css/all.css' %}" rel="stylesheet"/>
|
||||
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
|
||||
<script src="{% static 'js/jquery-3.7.1.min.js' %}"></script>
|
||||
|
||||
{% if context.is_demo %}
|
||||
<!-- Global Site Tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-37846380-3"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-37846380-3');
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -1,56 +0,0 @@
|
||||
{# vim: ft=htmldjango #}
|
||||
|
||||
{% load static %}
|
||||
|
||||
<div id="request-error" class="navbar navbar fixed-top">
|
||||
<div class="container">
|
||||
<div class="alert" style="display:none">
|
||||
<button class="close" type="button">×</button>
|
||||
<span class="msg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar navbar-header navbar-expand-lg fixed-top bg-dark">
|
||||
<div class="container">
|
||||
<a class="brand" href="/">
|
||||
<img src="{% static 'img/logo-full.svg' %}"/>
|
||||
</a>
|
||||
<ul class="nav float-right">
|
||||
{% if not up_to_date and not is_balena %}
|
||||
<li class="update-available">
|
||||
<a href="/settings#upgrade-section">
|
||||
<i class="fas fa-arrow-circle-down pr-1"></i>
|
||||
Update Available
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="/">
|
||||
<i class="far fa-clock pr-1"></i>
|
||||
Schedule Overview
|
||||
</a>
|
||||
</li>
|
||||
{% if is_balena %}
|
||||
<li>
|
||||
<a href="/integrations">
|
||||
<i class="far fa-plus-square pr-1"></i>
|
||||
Integrations
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="/settings">
|
||||
<i class="fas fa-cog pr-1"></i>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider-vertical"></li>
|
||||
<li>
|
||||
<a href="/system-info">
|
||||
<i class="fas fa-tasks pr-1"></i>
|
||||
System Info
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,386 +0,0 @@
|
||||
{# vim: ft=htmldjango #}
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<link href="{% static 'css/datepicker.css' %}" rel="stylesheet"/>
|
||||
<link href="{% static 'css/timepicker.css' %}" rel="stylesheet"/>
|
||||
|
||||
<script src="{% static 'js/underscore-1.4.3.min.js' %}"></script>
|
||||
<script src="{% static 'js/popper.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.iframe-transport.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/base64js.min.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/backbone-0.9.10.min.js' %}"></script> <!-- needs jquery -->
|
||||
|
||||
<script src="{% static 'js/jquery-ui-1.10.1.custom.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.fileupload.js' %}"></script>
|
||||
<!-- needs jqueryui.widget -->
|
||||
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script> <!-- needs jquery -->
|
||||
<script src="{% static 'js/bootstrap-datepicker.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap-timepicker.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/moment.js' %}"></script>
|
||||
|
||||
{{ ws_addresses|json_script:"ws_addresses" }};
|
||||
|
||||
<script type="text/javascript">
|
||||
var dateFormat = "{{ date_format }}";
|
||||
var defaultDuration = {{ default_duration }};
|
||||
var defaultStreamingDuration = {{ default_streaming_duration }};
|
||||
var use24HourClock = {% if use_24_hour_clock %} true; {% else %} false; {% endif %}
|
||||
var wsAddresses = JSON.parse(document.getElementById('ws_addresses').textContent);
|
||||
</script>
|
||||
|
||||
<script src="{% static 'dist/js/anthias.js' %}"></script>
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
|
||||
<script id="asset-row-template" type="text/template">
|
||||
<td class="asset_row_name">
|
||||
<i class="fas fa-grip-vertical mr-2"></i>
|
||||
<i class="asset-icon mr-2"></i>
|
||||
<%= name %>
|
||||
</td>
|
||||
<td style="width:21%">
|
||||
<%= start_date %>
|
||||
</td>
|
||||
<td style="width:21%">
|
||||
<%= end_date %>
|
||||
</td>
|
||||
<td style="width:13%">
|
||||
<%= duration %>
|
||||
</td>
|
||||
<td class="asset-toggle" style="width:7%">
|
||||
<label class="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
<input type="checkbox"/>
|
||||
<span>
|
||||
<span class="off"></span>
|
||||
<span class="on"></span>
|
||||
<a></a>
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="asset_row_btns">
|
||||
<button class="download-asset-button btn btn-outline-dark" type="button">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button class="edit-asset-button btn btn-outline-dark" type="button">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
<button class="delete-asset-button btn btn-outline-dark" data-html="true" data-placement="left"
|
||||
data-title="Are you sure?" data-trigger="manual" type="button">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</script>
|
||||
|
||||
<script id="confirm-delete-template" type="text/template">
|
||||
<div class="popover-delete-content">
|
||||
<div class="float-left">
|
||||
<a class="confirm-delete btn btn-danger" href="#">Delete</a>
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<a class="cancel-delete btn btn-outline-dark" href="#">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="processing-message-template" type="text/template">
|
||||
<label class="processing-message">Asset in processing</label>
|
||||
</script>
|
||||
|
||||
<script id="request-error-template" type="text/template">
|
||||
<div class="container">
|
||||
<div class="alert alert-danger">
|
||||
<button class="close" data-dismiss="alert" type="button">×</button>
|
||||
<span class="msg">
|
||||
The operation failed. Please reload the page and try again.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="request-success-template" type="text/template">
|
||||
<div class="container">
|
||||
<div class="alert alert-success">
|
||||
<button class="close" data-dismiss="alert" type="button">×</button>
|
||||
<span class="msg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="asset-modal-template" type="text/template">
|
||||
<div class="modal hide fade" aria-hidden="true" aria-labelledby="myModalLabel" role="dialog" tabindex="-1">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="form-horizontal">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalLabel">Add Asset</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="asset-location add">
|
||||
<fieldset>
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-tabs" id="add-asset-nav-tabs">
|
||||
<li class="tabnav-uri nav-item active show text-center">
|
||||
<a class="nav-link" href="#tab-uri">URL</a>
|
||||
</li>
|
||||
<li class="tabnav-file_upload nav-item text-center">
|
||||
<a class="nav-link" href="#tab-file_upload">Upload</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content px-4 pt-2 pb-4">
|
||||
<div id="tab-uri" class="tab-pane active"></div>
|
||||
<div id="tab-file_upload" class="tab-pane">
|
||||
<div class="control-group">
|
||||
<div class="filedrop">
|
||||
<div class="upload-header">
|
||||
<button class="btn btn-primary">Add Files</button>
|
||||
<input name="file_upload" type="file"/>
|
||||
<br/>
|
||||
or
|
||||
</div>
|
||||
<div>
|
||||
drop files here to upload
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<form id="add-form">
|
||||
<div class="form-group row uri">
|
||||
<label class="col-4 col-form-label">Asset URL</label>
|
||||
<div class="col-7 controls">
|
||||
<input class="form-control" name="uri"
|
||||
placeholder="Public URL to this asset's location" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row skip_asset_check_checkbox">
|
||||
<label class="col-4 small">Skip asset check</label>
|
||||
<div class="col-7 is_enabled-skip_asset_check_checkbox checkbox">
|
||||
<input name="skip_asset_check" type="checkbox" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-location edit" style="display:none">
|
||||
<div class="form-group row name">
|
||||
<label class="col-4 col-form-label">Name</label>
|
||||
<div class="col-7">
|
||||
<input class="form-control" name="name"
|
||||
placeholder="Nickname for this asset" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-4 col-form-label">Asset Location</label>
|
||||
<div class="col-8 controls">
|
||||
<div class="uri-text first"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mimetype">
|
||||
<label class="col-4 col-form-label">Asset Type</label>
|
||||
<div class="col-4 controls">
|
||||
<select class="mime-select form-control" name="mimetype">
|
||||
<option value="webpage">Webpage</option>
|
||||
<option value="image">Image</option>
|
||||
<option value="video">Video</option>
|
||||
<option value="streaming">Streaming</option>
|
||||
<option value="youtube_asset">YouTubeAsset</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row form-group loop_date">
|
||||
<label class="col-4 col-form-label">Play for</label>
|
||||
<div class="controls col-7">
|
||||
<select class="form-control" id="loop_times">
|
||||
<option value="day">1 Day</option>
|
||||
<option value="week">1 Week</option>
|
||||
<option value="month">1 Month</option>
|
||||
<option value="year">1 Year</option>
|
||||
<option value="forever">Forever</option>
|
||||
<option value="manual" selected="selected">Manual</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="manul_date">
|
||||
<div class="form-group row start_date">
|
||||
<label class="col-4 col-form-label">Start Date</label>
|
||||
<div class="controls col-7">
|
||||
<input class="form-control date" name="start_date_date" type="text"
|
||||
style="margin-right:5px"/>
|
||||
<input class="form-control time" name="start_date_time" type="text"/>
|
||||
</div>
|
||||
<input name="start_date" type="hidden"/>
|
||||
</div>
|
||||
<div class="form-group row end_date">
|
||||
<label class="col-4 col-form-label">End Date</label>
|
||||
<div class="controls col-7">
|
||||
<input class="form-control date" name="end_date_date" type="text"
|
||||
style="margin-right:5px"/>
|
||||
<input class="form-control time" name="end_date_time" type="text"/>
|
||||
</div>
|
||||
<input name="end_date" type="hidden"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row duration">
|
||||
<label class="col-4 col-form-label">Duration</label>
|
||||
<div class="col-7 controls">
|
||||
<input class="form-control" name="duration" type="number"/>
|
||||
seconds
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="advanced-accordion accordion">
|
||||
<div class="accordion-group">
|
||||
<div class="accordion-heading">
|
||||
<i class="fas fa-play unrotated"></i>
|
||||
<a class="advanced-toggle" href="#">Advanced</a>
|
||||
</div>
|
||||
<div class="collapse-advanced accordion-body collapse">
|
||||
<div class="accordion-inner">
|
||||
<div class="form-group row">
|
||||
<label class="col-4 col-form-label">Disable cache</label>
|
||||
<div class="col-8 nocache controls justify-content-center align-self-center">
|
||||
<label class="nocache-toggle toggle switch-light switch-ios small m-0">
|
||||
<input type="checkbox"/>
|
||||
<span><span></span><span></span><a></a></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="status"></div>
|
||||
<div class="float-left progress active" style="display:none">
|
||||
<div class="bar progress-bar-striped progress-bar progress-bar-animated"></div>
|
||||
</div>
|
||||
<input class="btn btn-outline-primary btn-long cancel" type="button" value="Cancel"/>
|
||||
<input id="save-asset" class="btn btn-primary btn-long" type="submit" value="Save"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="subscribe-form-template" type="text/template">
|
||||
<form class="validate"
|
||||
action="//wireload.us1.list-manage.com/subscribe/post?u=adb4f752497b0d86b3b1b79d7&id=5c47388076"
|
||||
method="post" name="mc-embedded-subscribe-form" style="margin-bottom:5px" target="_blank">
|
||||
<div class="mc-field-group form-group">
|
||||
<label for="mce-EMAIL">
|
||||
Email Address
|
||||
<span class="asterisk">*</span>
|
||||
</label>
|
||||
<input id="mce-EMAIL" class="required email form-control" name="EMAIL" required="required"
|
||||
type="email"/>
|
||||
</div>
|
||||
<div class="mc-field-group form-group">
|
||||
<label for="mce-FNAME">First Name</label>
|
||||
<input id="mce-FNAME" class="form-control" name="FNAME" type="text"/>
|
||||
</div>
|
||||
<div class="mc-field-group form-group">
|
||||
<label for="mce-LNAME">Last Name</label>
|
||||
<input id="mce-LNAME" class="form-control" name="LNAME" type="text"/>
|
||||
</div>
|
||||
<!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups -->
|
||||
<div hidden="true" style="position absolute; left -5000px;">
|
||||
<input name="b_adb4f752497b0d86b3b1b79d7_5c47388076" tabindex="-1" type="text"/>
|
||||
</div>
|
||||
<input id="mc-embedded-subscribe" class="button btn btn-outline-dark" name="subscribe" type="submit"
|
||||
value="Subscribe"/>
|
||||
</form>
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container pt-3 pb-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h4 class="d-flex">
|
||||
<b class="justify-content-center align-self-center text-white">Schedule Overview</b>
|
||||
<div class="ml-auto">
|
||||
<a id="previous-asset-button" class="btn btn-long btn-outline-primary" href="#">
|
||||
<i class="fas fa-chevron-left pr-2"></i>
|
||||
Previous Asset
|
||||
</a>
|
||||
<a id="next-asset-button" class="btn btn-long btn-outline-primary" href="#">
|
||||
Next Asset
|
||||
<i class="fas fa-chevron-right pl-2"></i>
|
||||
</a>
|
||||
<a id="add-asset-button" class="add-asset-button btn btn-long btn-primary" href="#">
|
||||
Add Asset
|
||||
</a>
|
||||
</div>
|
||||
</h4>
|
||||
{% if player_name %}
|
||||
<h4 class="text-white">{{ player_name }}</h4>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="assets">
|
||||
<div class="container">
|
||||
<div class="row content active-content px-2 pt-4">
|
||||
<div class="col-12 mb-5">
|
||||
<section id="active-assets-section">
|
||||
<h5>
|
||||
<b>Active assets</b>
|
||||
</h5>
|
||||
<table class="table">
|
||||
<thead class="table-borderless">
|
||||
<tr>
|
||||
<th class="font-weight-normal asset_row_name">Name</th>
|
||||
<th class="font-weight-normal" style="width:21%">Start</th>
|
||||
<th class="font-weight-normal" style="width:21%">End</th>
|
||||
<th class="font-weight-normal" style="width:13%">Duration</th>
|
||||
<th class="font-weight-normal" style="width:7%">Activity</th>
|
||||
<th class="font-weight-normal" style="width:13%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="active-assets"></tbody>
|
||||
</table>
|
||||
<div class="table-assets-help-text">Currently there are no assets. <a class="add-asset-button" href="#">Add asset</a> now.</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row content inactive-content px-2 pt-4">
|
||||
<div class="col-12 mb-5">
|
||||
<section id="inactive-assets-section">
|
||||
<h5>
|
||||
<b>Inactive assets</b>
|
||||
</h5>
|
||||
<table class="table">
|
||||
<thead class="table-borderless">
|
||||
<tr>
|
||||
<th class="text-secondary font-weight-normal asset_row_name">Name</th>
|
||||
<th class="text-secondary font-weight-normal" style="width:21%">Start</th>
|
||||
<th class="text-secondary font-weight-normal" style="width:21%">End</th>
|
||||
<th class="text-secondary font-weight-normal" style="width:13%">Duration</th>
|
||||
<th class="text-secondary font-weight-normal" style="width:7%">Activity</th>
|
||||
<th class="text-secondary font-weight-normal" style="width:13%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="inactive-assets"></tbody>
|
||||
</table>
|
||||
<div class="table-assets-help-text text-secondary">Currently there are no assets. <a class="add-asset-button" href="#">Add asset</a> now.</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
{% endblock %}
|
||||
@@ -1,65 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row py-2">
|
||||
<div class="col-12">
|
||||
<h4 class="page-header text-white">
|
||||
<b>Integrations</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row content" style="min-height: 60vh;">
|
||||
{% if is_balena %}
|
||||
<div id="balena-section" class="col-12">
|
||||
<h4 class="page-header">
|
||||
<b>Balena</b>
|
||||
</h4>
|
||||
<table class="table">
|
||||
<thead class="table-borderless">
|
||||
<tr>
|
||||
<th class="text-secondary font-weight-normal" scope="col">Option</th>
|
||||
<th class="text-secondary font-weight-normal" scope="col">Value</th>
|
||||
<th class="text-secondary font-weight-normal" scope="col">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Device Name</th>
|
||||
<td>{{ balena_device_name_at_init }}</td>
|
||||
<td>The name of the device on first initialisation.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Device UUID</th>
|
||||
<td>{{ balena_device_id }}</td>
|
||||
<td>The unique identification number for the device. This is used to identify it on
|
||||
balena.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">App ID</th>
|
||||
<td>{{ balena_app_id }}</td>
|
||||
<td>ID number of the balena application the device is associated.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">App Name</th>
|
||||
<td>{{ balena_app_name }}</td>
|
||||
<td>The name of the balena application the device is associated with.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Supervisor Version</th>
|
||||
<td>{{ balena_supervisor_version }}</td>
|
||||
<td>The current version of the supervisor agent running on the device.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Host OS Version</th>
|
||||
<td>{{ balena_host_os_version }}</td>
|
||||
<td>The version of the host OS.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
44
templates/react.html
Normal file
44
templates/react.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link
|
||||
href="{% static 'dist/css/anthias.css' %}"
|
||||
type="text/css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Plus Jakarta Sans"
|
||||
rel="stylesheet"
|
||||
>
|
||||
<link href="/static/favicons/apple-touch-icon-57x57.png" rel="apple-touch-icon-precomposed" sizes="57x57"/>
|
||||
<link href="/static/favicons/apple-touch-icon-114x114.png" rel="apple-touch-icon-precomposed" sizes="114x114"/>
|
||||
<link href="/static/favicons/apple-touch-icon-72x72.png" rel="apple-touch-icon-precomposed" sizes="72x72"/>
|
||||
<link href="/static/favicons/apple-touch-icon-144x144.png" rel="apple-touch-icon-precomposed" sizes="144x144"/>
|
||||
<link href="/static/favicons/apple-touch-icon-60x60.png" rel="apple-touch-icon-precomposed" sizes="60x60"/>
|
||||
<link href="/static/favicons/apple-touch-icon-120x120.png" rel="apple-touch-icon-precomposed" sizes="120x120"/>
|
||||
<link href="/static/favicons/apple-touch-icon-76x76.png" rel="apple-touch-icon-precomposed" sizes="76x76"/>
|
||||
<link href="/static/favicons/apple-touch-icon-152x152.png" rel="apple-touch-icon-precomposed" sizes="152x152"/>
|
||||
<link href="/static/favicons/favicon-196x196.png" rel="icon" sizes="196x196" type="image/png"/>
|
||||
<link href="/static/favicons/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/>
|
||||
<link href="/static/favicons/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/>
|
||||
<link href="/static/favicons/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/>
|
||||
<link href="/static/favicons/favicon-128.png" rel="icon" sizes="128x128" type="image/png"/>
|
||||
<link href="https://fonts.googleapis.com/css?family=Plus Jakarta Sans" rel="stylesheet">
|
||||
<link rel="icon" href="/static/favicons/favicon.ico">
|
||||
<meta content="anthias;" name="application-name"/>
|
||||
<meta content="#FFFFFF" name="msapplication-TileColor"/>
|
||||
<meta content="/static/favicons/mstile-144x144.png" name="msapplication-TileImage"/>
|
||||
<meta content="/static/favicons/mstile-70x70.png" name="msapplication-square70x70logo"/>
|
||||
<meta content="/static/favicons/mstile-150x150.png" name="msapplication-square150x150logo"/>
|
||||
<meta content="/static/favicons/mstile-310x150.png" name="msapplication-wide310x150logo"/>
|
||||
<meta content="/static/favicons/mstile-310x310.png" name="msapplication-square310x310logo"/>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="{% static 'dist/js/anthias.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,330 +0,0 @@
|
||||
{# vim: ft=htmldjango #}
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'js/popper.min.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script> <!-- needs jquery -->
|
||||
<script src="{% static 'js/jquery-ui-1.10.1.custom.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.fileupload.js' %}"></script> <!-- needs jqueryui.widget -->
|
||||
<script src="{% static 'js/bootstrap-datepicker.js' %}"></script>
|
||||
|
||||
<script src="{% static 'dist/js/settings.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container"
|
||||
>
|
||||
<div class="row py-2">
|
||||
<div class="col-12">
|
||||
<h4 class="page-header text-white">
|
||||
<b>Settings</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row content px-3">
|
||||
<div class="col-12 my-3">
|
||||
{% if flash %}
|
||||
<div class="alert alert-{{ flash.class }}">
|
||||
{{ flash.message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="row">
|
||||
{% csrf_token %}
|
||||
<div class="form-group col-6 d-flex flex-column justify-content-between">
|
||||
<div class="form-group">
|
||||
<label class="small text-secondary">Player name</label>
|
||||
<input class="form-control" name="player_name" type="text"
|
||||
value="{{ player_name }}">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label class="small text-secondary">Default duration (seconds)</label>
|
||||
<input class="form-control" name="default_duration" type="number"
|
||||
value="{{ default_duration }}"/>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label class="small text-secondary">Default streaming duration (seconds)</label>
|
||||
<input class="form-control" name="default_streaming_duration" type="number"
|
||||
value="{{ default_streaming_duration }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="small text-secondary">Audio output</label>
|
||||
<select class="form-control" name="audio_output">
|
||||
{% if audio_output == 'hdmi' %}
|
||||
<option value="hdmi" selected="selected">HDMI</option>
|
||||
{% if device_type != 'pi5' %}
|
||||
<option value="local">3.5mm jack</option>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<option value="hdmi">HDMI</option>
|
||||
{% if device_type != 'pi5' %}
|
||||
<option value="local" selected="selected">3.5mm jack</option>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="small text-secondary">Date format</label>
|
||||
<select class="form-control" name="date_format">
|
||||
<option value="mm/dd/yyyy"
|
||||
{% if date_format == 'mm/dd/yyyy' %} selected="selected" {% endif %}>
|
||||
month/day/year
|
||||
</option>
|
||||
<option value="dd/mm/yyyy"
|
||||
{% if date_format == 'dd/mm/yyyy' %} selected="selected" {% endif %}>
|
||||
day/month/year
|
||||
</option>
|
||||
<option value="yyyy/mm/dd"
|
||||
{% if date_format == 'yyyy/mm/dd' %} selected="selected" {% endif %}>
|
||||
year/month/day
|
||||
</option>
|
||||
<option value="mm-dd-yyyy"
|
||||
{% if date_format == 'mm-dd-yyyy' %} selected="selected" {% endif %}>
|
||||
month-day-year
|
||||
</option>
|
||||
<option value="dd-mm-yyyy"
|
||||
{% if date_format == 'dd-mm-yyyy' %} selected="selected" {% endif %}>
|
||||
day-month-year
|
||||
</option>
|
||||
<option value="yyyy-mm-dd"
|
||||
{% if date_format == 'yyyy-mm-dd' %} selected="selected" {% endif %}>
|
||||
year-month-day
|
||||
</option>
|
||||
<option value="mm.dd.yyyy"
|
||||
{% if date_format == 'mm.dd.yyyy' %} selected="selected" {% endif %}>
|
||||
month.day.year
|
||||
</option>
|
||||
<option value="dd.mm.yyyy"
|
||||
{% if date_format == 'dd.mm.yyyy' %} selected="selected" {% endif %}>
|
||||
day.month.year
|
||||
</option>
|
||||
<option value="yyyy.mm.dd"
|
||||
{% if date_format == 'yyyy.mm.dd' %} selected="selected" {% endif %}>
|
||||
year.month.day
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="small text-secondary">Authentication</label>
|
||||
<select class="form-control" id="auth_backend" name="auth_backend">
|
||||
{% for opt in auth_backends %}
|
||||
<option value="{{ opt.name }}" {{ opt.selected }}>{{ opt.text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% if need_current_password %}
|
||||
<div class="form-group" id="curpassword_group">
|
||||
<label class="small text-secondary">Current Password</label>
|
||||
<input class="form-control" name="current-password" type="password" value="">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for backend in auth_backends %}
|
||||
{% if backend.template %}
|
||||
<div id="auth_backend-{{ backend.name }}">
|
||||
{% include backend.template %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-group col-6 d-flex flex-column justify-content-start">
|
||||
<div class="form-inline mt-4">
|
||||
<label>Show splash screen</label>
|
||||
<div class="ml-auto">
|
||||
<label id="splash_checkbox"
|
||||
class="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
{% if show_splash %}
|
||||
<input name="show_splash" checked="checked" type="checkbox"/>
|
||||
{% else %}
|
||||
<input name="show_splash" type="checkbox"/>
|
||||
{% endif %}
|
||||
<span><span></span><span></span><a></a></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline mt-4">
|
||||
<label>Default assets</label>
|
||||
<div class="ml-auto">
|
||||
<label id="default_assets_checkbox"
|
||||
class="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
{% if default_assets %}
|
||||
<input name="default_assets" checked="checked" type="checkbox"/>
|
||||
{% else %}
|
||||
<input name="default_assets" type="checkbox"/>
|
||||
{% endif %}
|
||||
<span><span></span><span></span><a></a></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline mt-4">
|
||||
<label>Shuffle playlist</label>
|
||||
<div class="ml-auto">
|
||||
<label id="shuffle_checkbox"
|
||||
class="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
{% if shuffle_playlist %}
|
||||
<input name="shuffle_playlist" checked="checked" type="checkbox"/>
|
||||
{% else %}
|
||||
<input name="shuffle_playlist" type="checkbox"/>
|
||||
{% endif %}
|
||||
<span><span></span><span></span><a></a></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline mt-4">
|
||||
<label>Use 24-hour clock</label>
|
||||
<div class="ml-auto">
|
||||
<label id="use_24_hour_clock_checkbox"
|
||||
class="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
{% if use_24_hour_clock %}
|
||||
<input name="use_24_hour_clock" checked="checked" type="checkbox"/>
|
||||
{% else %}
|
||||
<input name="use_24_hour_clock" type="checkbox"/>
|
||||
{% endif %}
|
||||
<span><span></span><span></span><a></a></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline mt-4">
|
||||
<label>Debug logging</label>
|
||||
<div class="ml-auto">
|
||||
<label id="debug_checkbox"
|
||||
class="is_enabled-toggle toggle switch-light switch-material small m-0">
|
||||
{% if debug_logging %}
|
||||
<input name="debug_logging" checked="checked" type="checkbox">
|
||||
{% else %}
|
||||
<input name="debug_logging" type="checkbox">
|
||||
{% endif %}
|
||||
<span><span></span><span></span><a></a></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-6 offset-6">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12">
|
||||
<div class="text-right">
|
||||
<a class="btn btn-long btn-outline-primary" href="/">Cancel</a>
|
||||
<input class="btn btn-long btn-primary" type="submit" value="Save Settings">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not context.up_to_date and not context.is_balena and context.is_docker %}
|
||||
<div class="container mt-4">
|
||||
<div class="row py-2">
|
||||
<div class="col-12">
|
||||
<h4 class="page-header text-white">
|
||||
<b>Upgrade Anthias</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row content px-3">
|
||||
<div id="upgrade-section" class="col-12 my-3">
|
||||
<p>Do the following steps to upgrade Anthias:</p>
|
||||
<ul>
|
||||
<li>
|
||||
Go to the <a href="#backup-section" class="text-danger">backup section</a> and click <em>Get Backup</em>.
|
||||
</li>
|
||||
<li>
|
||||
Open up a terminal and SSH to this device using any of the following commands:
|
||||
<ul>
|
||||
{% for ip_address in context.ip_addresses %}
|
||||
<li><code>ssh {{ context.host_user }}@{{ ip_address }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Go to the project root directory — <code>cd ~/screenly</code>
|
||||
</li>
|
||||
<li>
|
||||
Run the following upgrade script — <code>./bin/run_upgrade.sh</code>. The script is essentially a wrapper around
|
||||
the install script, so it will prompt you with the same questions as when you first installed Anthias.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row py-2">
|
||||
<div class="col-12">
|
||||
<h4 class="page-header text-white">
|
||||
<b>Backup</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row content px-3">
|
||||
{# Backup #}
|
||||
<div id="backup-section" class="col-12 my-3">
|
||||
<div class="text-right">
|
||||
<input name="backup_upload" style="display:none" type="file">
|
||||
<button id="btn-backup" class="btn btn-long btn-outline-primary">Get Backup</button>
|
||||
<button id="btn-upload" class="btn btn-primary" type="button">Upload and Recover
|
||||
</button>
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-striped progress active w-100" style="display:none">
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not is_balena and not is_docker %}
|
||||
{# Reset Wifi #}
|
||||
<div class="container mt-4">
|
||||
<div class="row py-2">
|
||||
<div class="col-12">
|
||||
<h4 class="page-header text-white">
|
||||
<b>Reset Wi-Fi Config</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row content px-3">
|
||||
<div id="wifi-section" class="col-12 my-3">
|
||||
<p>Run it, and if the next boot does not have a network connection, you will be prompted to select a
|
||||
WiFi
|
||||
network. <b>Warning:</b> After pressing, a reboot is required. Web interface will not be
|
||||
available
|
||||
until reboot.</p>
|
||||
<div class="text-right">
|
||||
<button id="btn-reset" class="btn btn-danger btn-long" type="button">Reset Wi-Fi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# System controls #}
|
||||
<div class="container mt-4">
|
||||
<div class="row py-2">
|
||||
<div class="col-12">
|
||||
<h4 class="page-header text-white">
|
||||
<b>System Controls</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row content px-3">
|
||||
<div id="system-controls-section" class="col-12 my-3">
|
||||
<div class="text-right">
|
||||
<button id="btn-reboot-system" class="btn btn-danger btn-long" type="button">Reboot</button>
|
||||
<button id="btn-shutdown-system" class="btn btn-danger btn-long" type="button">Shutdown</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,80 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row py-2">
|
||||
<div class="col-12">
|
||||
<h4 class="page-header text-white">
|
||||
<b>System Info</b>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row content">
|
||||
<div class="col-12">
|
||||
|
||||
<table class="table mb-5">
|
||||
<thead class="table-borderless">
|
||||
<tr>
|
||||
<th class="text-secondary font-weight-normal" scope="col" style="width: 20%">Option</th>
|
||||
<th class="text-secondary font-weight-normal" scope="col">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Load Average</th>
|
||||
<td>{{ loadavg }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Free Space</th>
|
||||
<td>{{ free_space }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Memory</th>
|
||||
<td>
|
||||
Total: <strong>{{ memory.total }}</strong> /
|
||||
Used: <strong>{{ memory.used }}</strong> /
|
||||
Free: <strong>{{ memory.free }}</strong> /
|
||||
Shared: <strong>{{ memory.shared }}</strong> /
|
||||
Buff: <strong>{{ memory.buff }}</strong> /
|
||||
Available: <strong>{{ memory.available }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Uptime</th>
|
||||
<td>{{ uptime.days }} days and {{ uptime.hours }} hours</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Display Power (CEC)</th>
|
||||
<td>{{ display_power }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Device Model</th>
|
||||
<td>{{ device_model }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Anthias Version</th>
|
||||
<td>
|
||||
{% if anthias_commit_link %}
|
||||
<a
|
||||
href="{{ anthias_commit_link }}"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
class="text-dark"
|
||||
>
|
||||
{{ anthias_version }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ anthias_version }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">MAC Address</th>
|
||||
<td>{{ mac_address }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -103,7 +103,7 @@ class WebTest(TestCase):
|
||||
lambda field: field.fill('https://example.com'))
|
||||
sleep(1)
|
||||
|
||||
wait_for_and_do(browser, '#add-form', lambda form: form.click())
|
||||
wait_for_and_do(browser, '#tab-uri', lambda form: form.click())
|
||||
sleep(1) # Wait for the new-asset panel animation.
|
||||
|
||||
wait_for_and_do(browser, '#save-asset', lambda btn: btn.click())
|
||||
@@ -118,6 +118,7 @@ class WebTest(TestCase):
|
||||
self.assertEqual(asset.mimetype, 'webpage')
|
||||
self.assertEqual(asset.duration, settings['default_duration'])
|
||||
|
||||
@skip('migrate to React-based tests')
|
||||
def test_edit_asset(self):
|
||||
asset = Asset.objects.create(**asset_x)
|
||||
|
||||
@@ -132,10 +133,14 @@ class WebTest(TestCase):
|
||||
lambda field: field.fill('333'))
|
||||
sleep(1)
|
||||
|
||||
wait_for_and_do(browser, '#add-form', lambda form: form.click())
|
||||
sleep(1)
|
||||
wait_for_and_do(browser, '#edit-form', lambda form: form.click())
|
||||
sleep(3)
|
||||
|
||||
wait_for_and_do(browser, '#save-asset', lambda btn: btn.click())
|
||||
wait_for_and_do(
|
||||
browser,
|
||||
'.edit-asset-modal #save-asset',
|
||||
lambda btn: btn.click()
|
||||
)
|
||||
sleep(3)
|
||||
|
||||
assets = Asset.objects.all()
|
||||
@@ -154,7 +159,7 @@ class WebTest(TestCase):
|
||||
sleep(1)
|
||||
|
||||
wait_for_and_do(
|
||||
browser, 'a[href="#tab-file_upload"]', lambda tab: tab.click())
|
||||
browser, '.nav-link.upload-asset-tab', lambda tab: tab.click())
|
||||
wait_for_and_do(
|
||||
browser, 'input[name="file_upload"]',
|
||||
lambda input: input.fill(image_file))
|
||||
@@ -181,7 +186,7 @@ class WebTest(TestCase):
|
||||
sleep(1)
|
||||
|
||||
wait_for_and_do(
|
||||
browser, 'a[href="#tab-file_upload"]',
|
||||
browser, '.nav-link.upload-asset-tab',
|
||||
lambda tab: tab.click())
|
||||
wait_for_and_do(
|
||||
browser, 'input[name="file_upload"]',
|
||||
@@ -211,7 +216,7 @@ class WebTest(TestCase):
|
||||
sleep(1)
|
||||
|
||||
wait_for_and_do(
|
||||
browser, 'a[href="#tab-file_upload"]',
|
||||
browser, '.nav-link.upload-asset-tab',
|
||||
lambda tab: tab.click())
|
||||
wait_for_and_do(
|
||||
browser, 'input[name="file_upload"]',
|
||||
@@ -265,6 +270,7 @@ class WebTest(TestCase):
|
||||
self.assertEqual(
|
||||
asset.duration, settings['default_streaming_duration'])
|
||||
|
||||
@skip('migrate to React-based tests')
|
||||
def test_remove_asset(self):
|
||||
Asset.objects.create(**asset_x)
|
||||
|
||||
@@ -311,6 +317,7 @@ class WebTest(TestCase):
|
||||
asset = assets.first()
|
||||
self.assertEqual(asset.is_enabled, 0)
|
||||
|
||||
@skip('migrate to React-based tests')
|
||||
def test_reorder_asset(self):
|
||||
Asset.objects.create(**{
|
||||
**asset_x,
|
||||
|
||||
@@ -1,36 +1,56 @@
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const path = require('path');
|
||||
const path = require("path");
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
'anthias': './static/js/anthias.coffee',
|
||||
'settings': './static/js/settings.coffee',
|
||||
"anthias": "./static/src/index.js",
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'static/dist'),
|
||||
filename: 'js/[name].js',
|
||||
path: path.resolve(__dirname, "static/dist"),
|
||||
filename: "js/[name].js",
|
||||
clean: true,
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'css/anthias.css'
|
||||
})
|
||||
filename: "css/anthias.css"
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
React: 'react'
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.coffee$/,
|
||||
use: ['coffee-loader']
|
||||
test: /.(js|jsx|mjs)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-react'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
"css-loader",
|
||||
"sass-loader"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@/components': path.resolve(__dirname, 'static/src/components'),
|
||||
'@/store': path.resolve(__dirname, 'static/src/store'),
|
||||
'@/sass': path.resolve(__dirname, 'static/sass'),
|
||||
'@/utils': path.resolve(__dirname, 'static/src/utils'),
|
||||
},
|
||||
extensions: ['.js', '.jsx']
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
const { merge } = require("webpack-merge");
|
||||
const common = require("./webpack.common.js");
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: 'source-map',
|
||||
mode: 'development',
|
||||
devtool: "source-map",
|
||||
mode: "development",
|
||||
devServer: {
|
||||
contentBase: "./static/dist",
|
||||
hot: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user