Merge branch 'v2.0.0-dev'
26
.gitignore
vendored
@ -1,15 +1,15 @@
|
|||||||
clips/
|
.ropeproject
|
||||||
media
|
**temp
|
||||||
media/
|
*.log*
|
||||||
public
|
.DS_Store
|
||||||
public/
|
__pycache__/
|
||||||
ADtvMedia
|
venv/
|
||||||
ADtvMedia/
|
*-orig.*
|
||||||
live
|
tests/
|
||||||
live/
|
|
||||||
playlists
|
|
||||||
playlists/
|
|
||||||
tmp
|
|
||||||
tmp/
|
|
||||||
._*
|
._*
|
||||||
._**
|
._**
|
||||||
|
*.sqlite3
|
||||||
|
**/migrations/*
|
||||||
|
!**/migrations/__init__.py
|
||||||
|
**/ffplayout/apps/*
|
||||||
|
!**/ffplayout/apps/api_player/
|
||||||
|
56
README.md
@ -1,50 +1,36 @@
|
|||||||
ffplayout-gui
|
ffplayout-gui
|
||||||
=====
|
=====
|
||||||
|
|
||||||
Web-based graphical user interface for [ffplayout-engine](https://github.com/ffplayout/ffplayout-engine).
|
This is the fresh version v2.0.0-beta from ffplayout GUI.
|
||||||
|
|
||||||
![ffplayout-gui](./ffplayout-gui.png)
|
At the moment this version is more for testing. If you want to test it check [install.md](docs/install.md) for installation.
|
||||||
|
|
||||||
**Warning:** This GUI is **not** made with security in mind. It have some potential risks and should only be used in local environments.
|
A old working version you found in v1.0.0 branch, or under releases.
|
||||||
|
|
||||||
The GUI uses CSS Grid, so it only works in modern Browsers, in Internet Explorer 11 for example it will not work.
|
**This version is not production ready!!**
|
||||||
|
|
||||||
Minimum Screen Resolution is 1920*1080.
|
## some impressions below:
|
||||||
|
|
||||||
In some parts, like list_op.php the code is a bit messy. I use here formating options for our file names.
|
#### Login
|
||||||
|
![login](/assets/login.png)
|
||||||
|
|
||||||
Help in code inprovements are very welcome!
|
#### Landing Page
|
||||||
|
![landing-page](/assets/landing-page.png)
|
||||||
|
|
||||||
ffplayout and ffplayout-gui is only tested on linux. My setup runs on debian, so the instruction is made for it to.
|
#### Control Page
|
||||||
|
![control](/assets/control.png)
|
||||||
|
|
||||||
For previewing the clips, they must have a format what video.js can read. I use here h264/aac/mp4.
|
#### Media Page
|
||||||
|
![media](/assets/media.png)
|
||||||
|
|
||||||
Used Modules:
|
#### Media Page / Upload
|
||||||
-----
|
![media-upload](/assets/media-upload.png)
|
||||||
- https://github.com/RubaXa/Sortable
|
|
||||||
- https://github.com/DirectoryLister/DirectoryLister
|
|
||||||
- https://github.com/KennethanCeyer/pg-calendar
|
|
||||||
- https://github.com/videojs
|
|
||||||
- https://github.com/videojs/videojs-contrib-hls
|
|
||||||
- https://github.com/videojs/videojs-flash
|
|
||||||
- http://jquery.com/
|
|
||||||
- https://jqueryui.com/
|
|
||||||
- https://momentjs.com/
|
|
||||||
|
|
||||||
|
#### Message Page
|
||||||
|
![message](/assets/message.png)
|
||||||
|
|
||||||
Installation:
|
#### Logging Page
|
||||||
----
|
![logging](/assets/logging.png)
|
||||||
- ffplayout should be setup already
|
|
||||||
- install a webserver with php support (Apache or nginx)
|
|
||||||
- install [srs](https://github.com/ossrs/srs) or something similar for previewing your stream (example for srs you found in the wiki)
|
|
||||||
- we need some visudo entries:
|
|
||||||
- www-data ALL = NOPASSWD: /bin/systemctl start srs, /bin/systemctl stop srs, /bin/systemctl status srs
|
|
||||||
- www-data ALL = NOPASSWD: /bin/systemctl start ffplayout, /bin/systemctl stop ffplayout, /bin/systemctl status ffplayout, /bin/journalctl -u ffplayout.service -n 1000
|
|
||||||
- place ffplayout-gui in your **www** folder and make it reachable for your network
|
|
||||||
- symlink your media folder to the root folder (the author from DirectoryLister don't recommend this way...):
|
|
||||||
- ln -s /AtvMedia /var/www/ffplayout/
|
|
||||||
- the ffplayout-gui need read access to /etc/ffplayout/ffplayout.conf
|
|
||||||
- the gui needs also read access to the ffplayout log file
|
|
||||||
- that's it
|
|
||||||
|
|
||||||
I hope the GUI is self explaining, when not I can write something to for it.
|
#### Configuration Page / GUI
|
||||||
|
![config-gui](/assets/config-gui.png)
|
||||||
|
BIN
assets/config-gui.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
assets/control.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
assets/landing-page.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
assets/logging.png
Normal file
After Width: | Height: | Size: 378 KiB |
BIN
assets/login.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
assets/media-upload.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
assets/media.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
assets/message.png
Normal file
After Width: | Height: | Size: 61 KiB |
83
docs/db_data.json
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
[{
|
||||||
|
"model": "api_player.guisettings",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"player_url": "http://localhost/live/stream.m3u8",
|
||||||
|
"playout_config": "/etc/ffplayout/ffplayout.yml",
|
||||||
|
"net_interface": "ens3",
|
||||||
|
"media_disk": "/",
|
||||||
|
"extra_extensions": ".jpg .jpeg .png"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "api_player.messengepresets",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "default",
|
||||||
|
"message": "Wellcome to ffplayout!",
|
||||||
|
"x": "(w-text_w)/2",
|
||||||
|
"y": "(h-text_h)/2",
|
||||||
|
"font_size": 24,
|
||||||
|
"font_spacing": 4,
|
||||||
|
"font_color": "#ffffff",
|
||||||
|
"font_alpha": 1.0,
|
||||||
|
"show_box": true,
|
||||||
|
"box_color": "#000000",
|
||||||
|
"box_alpha": 0.8,
|
||||||
|
"border_width": 4,
|
||||||
|
"overall_alpha": "1"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "api_player.messengepresets",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "empty-text",
|
||||||
|
"message": null,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"font_size": 24,
|
||||||
|
"font_spacing": 4,
|
||||||
|
"font_color": "#ffffff",
|
||||||
|
"font_alpha": 1.0,
|
||||||
|
"show_box": false,
|
||||||
|
"box_color": "#000000",
|
||||||
|
"box_alpha": 0.8,
|
||||||
|
"border_width": 0,
|
||||||
|
"overall_alpha": "0"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "api_player.messengepresets",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "bottom-text-fade-in",
|
||||||
|
"message": "The upcoming event will be delayed by a few minutes.",
|
||||||
|
"x": "(w-text_w)/2",
|
||||||
|
"y": "(h-line_h)*0.9",
|
||||||
|
"font_size": 24,
|
||||||
|
"font_spacing": 4,
|
||||||
|
"font_color": "#ffffff",
|
||||||
|
"font_alpha": 1.0,
|
||||||
|
"show_box": true,
|
||||||
|
"box_color": "#000000",
|
||||||
|
"box_alpha": 0.8,
|
||||||
|
"border_width": 4,
|
||||||
|
"overall_alpha": "ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),0,if(lt(t,ld(1)+2),(t-(ld(1)+1))/1,if(lt(t,ld(1)+8),1,if(lt(t,ld(1)+9),(1-(t-(ld(1)+8)))/1,0))))"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "api_player.messengepresets",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "scrolling-text",
|
||||||
|
"message": "We have a very important announcement to make.",
|
||||||
|
"x": "ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),w+4,w-w/12*mod(t-ld(1),12*(w+tw)/w))",
|
||||||
|
"y": "(h-line_h)*0.9",
|
||||||
|
"font_size": 24,
|
||||||
|
"font_spacing": 4,
|
||||||
|
"font_color": "#ffffff",
|
||||||
|
"font_alpha": 1.0,
|
||||||
|
"show_box": true,
|
||||||
|
"box_color": "#000000",
|
||||||
|
"box_alpha": 0.8,
|
||||||
|
"border_width": 4,
|
||||||
|
"overall_alpha": "1"
|
||||||
|
}
|
||||||
|
}]
|
15
docs/developer-info.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
### Backend Apps
|
||||||
|
|
||||||
|
If you planing to extend the backend with your own apps (api endpoints),
|
||||||
|
just add your app in folder: **ffplayout/apps**.
|
||||||
|
|
||||||
|
If you planing to us a DB, put a **settings.py** file in your app. With this object:
|
||||||
|
|
||||||
|
```python
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'db-name.sqlite3'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
14
docs/ffplayout-api.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ffplayout api daemon
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/var/www/ffplayout/ffplayout
|
||||||
|
Environment=DJANGO_SETTINGS_MODULE=ffplayout.settings.production
|
||||||
|
ExecStart=/var/www/ffplayout/venv/bin/gunicorn --workers 6 --timeout 300 --log-level=info --log-file=- --access-logfile=- --bind unix:/var/www/ffplayout/ffplayout/ffplayout.sock ffplayout.wsgi:application
|
||||||
|
KillMode=process
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
65
docs/ffplayout.conf
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
map $sent_http_content_type $expires {
|
||||||
|
"text/html" 1h; # set this to your needs
|
||||||
|
"text/html; charset=utf-8" 1h; # set this to your needs
|
||||||
|
default 1d; # set this to your needs
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.1.4;
|
||||||
|
|
||||||
|
server_name ffplayout.local ffplayout;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain application/xml text/css application/javascript;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
client_max_body_size 7000M; # should be desirable value
|
||||||
|
|
||||||
|
add_header X-Frame-Options SAMEORIGIN;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
if ($http_origin ~ '^https?://(localhost|ffplayout\.local|ffplayout)') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS ) {
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
root /var/www/ffplayout/ffplayout/frontend/dist/;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(api|admin) {
|
||||||
|
if ($http_origin ~ '^https?://(localhost|ffplayout\.local|ffplayout)') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_pass http://unix:/var/www/ffplayout/ffplayout/ffplayout.sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /var/www/ffplayout/ffplayout/static/;
|
||||||
|
}
|
||||||
|
|
||||||
|
access_log /var/log/nginx/ffplayout_access.log;
|
||||||
|
error_log /var/log/nginx/ffplayout_error.log warn;
|
||||||
|
}
|
64
docs/install.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Manuel Installation Guide
|
||||||
|
|
||||||
|
**We are assuming that the system user `www-data` will run all processes!**
|
||||||
|
|
||||||
|
### API Setup
|
||||||
|
|
||||||
|
##### Preparation
|
||||||
|
- clone repo to `/var/www/ffplayout`
|
||||||
|
- cd in root folder from repo
|
||||||
|
- add virtual environment: `virtualenv -p python3 venv`
|
||||||
|
- run `source ./venv/bin/activate`
|
||||||
|
- install dependencies: `pip install -r requirements-base.txt`
|
||||||
|
- cd in `ffplayout`
|
||||||
|
- generate and copy secret: `python manage.py shell -c 'from django.core.management import utils; print(utils.get_random_secret_key())'`
|
||||||
|
- open **ffplayout/settings/production.py**
|
||||||
|
- past secret key in variable `SECRET_KEY`
|
||||||
|
- set `ALLOWED_HOSTS` with correct URL
|
||||||
|
- set URL in `CORS_ORIGIN_WHITELIST`
|
||||||
|
- migrate database: `python manage.py makemigrations && python manage.py migrate`
|
||||||
|
- collect static files: `python manage.py collectstatic`
|
||||||
|
- add super user to db: `python manage.py createsuperuser`
|
||||||
|
- populate some data to db: `python manage.py loaddata ../docs/db_data.json`
|
||||||
|
- run: `chown www-data. -R /var/www/ffplayout`
|
||||||
|
|
||||||
|
##### System Setup
|
||||||
|
- copy **docs/ffplayout-api.service** from root folder to **/etc/systemd/system/**
|
||||||
|
- enable service and start it: `systemctl enable ffplayout-api.service && systemctl start ffplayout-api.service`
|
||||||
|
- install **nginx**
|
||||||
|
- edit **docs/ffplayout.conf**
|
||||||
|
- set correct IP and `server_name`
|
||||||
|
- add domain `http_origin` test value
|
||||||
|
- add https redirection and SSL if is necessary
|
||||||
|
- copy **docs/ffplayout.conf** to **/etc/nginx/sites-available/**
|
||||||
|
- symlink config: `ln -s /etc/nginx/sites-available/ffplayout.conf /etc/nginx/sites-enabled/`
|
||||||
|
- restart nginx
|
||||||
|
- run `visudo` and add:
|
||||||
|
```
|
||||||
|
www-data ALL = NOPASSWD: /bin/systemctl start ffplayout-engine.service, /bin/systemctl stop ffplayout-engine.service, /bin/systemctl reload ffplayout-engine.service, /bin/systemctl restart ffplayout-engine.service, /bin/systemctl status ffplayout-engine.service, /bin/systemctl is-active ffplayout-engine.service, /bin/journalctl -n 1000 -u ffplayout-engine.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
**We need a recent version of npm**
|
||||||
|
|
||||||
|
- go to folder **/var/www/ffplayout/ffplayout/frontend**
|
||||||
|
- install dependencies: `npm install`
|
||||||
|
- build app: `npm run build`
|
||||||
|
|
||||||
|
Your frontend should be now in **/var/www/ffplayout/ffplayout/frontend/dist** folder, which we are included already in the nginx config. You can serve now the GUI under your domain URL.
|
||||||
|
|
||||||
|
### OS Specific
|
||||||
|
On debian 10 you need to install:
|
||||||
|
|
||||||
|
```
|
||||||
|
apt install -y curl
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -sL https://deb.nodesource.com/setup_14.x | bash -
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
apt install -y sudo net-tools git python3-dev build-essential virtualenv python3-virtualenv nodejs nginx autoconf automake libtool pkg-config texi2html yasm cmake curl mercurial git wget gperf
|
||||||
|
```
|
Before Width: | Height: | Size: 274 KiB |
0
ffplayout/apps/api_player/__init__.py
Normal file
21
ffplayout/apps/api_player/admin.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from apps.api_player.models import GuiSettings, MessengePresets
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
|
||||||
|
class GuiSettingsAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GuiSettings
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class MessengePresetsAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name',)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MessengePresets
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(GuiSettings, GuiSettingsAdmin)
|
||||||
|
admin.site.register(MessengePresets, MessengePresetsAdmin)
|
5
ffplayout/apps/api_player/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ApiPlayerConfig(AppConfig):
|
||||||
|
name = 'api_player'
|
0
ffplayout/apps/api_player/migrations/__init__.py
Normal file
70
ffplayout/apps/api_player/models.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import psutil
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class GuiSettings(models.Model):
|
||||||
|
"""
|
||||||
|
Here we manage the settings for the web GUI:
|
||||||
|
- Player URL
|
||||||
|
- settings for the statistics
|
||||||
|
"""
|
||||||
|
|
||||||
|
addrs = psutil.net_if_addrs()
|
||||||
|
addrs = [(i, i) for i in addrs.keys()]
|
||||||
|
|
||||||
|
player_url = models.CharField(max_length=255)
|
||||||
|
playout_config = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
default='/etc/ffplayout/ffplayout.yml')
|
||||||
|
net_interface = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=addrs,
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
media_disk = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
help_text="should be a mount point, for statistics",
|
||||||
|
default='/')
|
||||||
|
extra_extensions = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
help_text="file extensions, that are only visible in GUI",
|
||||||
|
blank=True, null=True, default='')
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.pk is not None or GuiSettings.objects.count() == 0:
|
||||||
|
super(GuiSettings, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
if not self.related_query.all():
|
||||||
|
super(GuiSettings, self).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "guisettings"
|
||||||
|
|
||||||
|
|
||||||
|
class MessengePresets(models.Model):
|
||||||
|
name = models.CharField(max_length=255, help_text="the preset name")
|
||||||
|
|
||||||
|
message = models.CharField(
|
||||||
|
max_length=1024, blank=True, null=True, default='')
|
||||||
|
|
||||||
|
x = models.CharField(
|
||||||
|
max_length=512, blank=True, null=True, default='')
|
||||||
|
|
||||||
|
y = models.CharField(
|
||||||
|
max_length=512, blank=True, null=True, default='')
|
||||||
|
|
||||||
|
font_size = models.IntegerField(default=24)
|
||||||
|
font_spacing = models.IntegerField(default=4)
|
||||||
|
font_color = models.CharField(max_length=12, default='#ffffff')
|
||||||
|
font_alpha = models.FloatField(default=1.0)
|
||||||
|
show_box = models.BooleanField(default=True)
|
||||||
|
box_color = models.CharField(max_length=12, default='#000000')
|
||||||
|
box_alpha = models.FloatField(default=0.8)
|
||||||
|
border_width = models.IntegerField(default=4)
|
||||||
|
overall_alpha = models.CharField(
|
||||||
|
max_length=255, blank=True, null=True, default='')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "messengepresets"
|
61
ffplayout/apps/api_player/serializers.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from apps.api_player.models import GuiSettings, MessengePresets
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
new_password = serializers.CharField(write_only=True, required=False)
|
||||||
|
old_password = serializers.CharField(write_only=True, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['id', 'username', 'old_password',
|
||||||
|
'new_password', 'email']
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
print(validated_data)
|
||||||
|
instance.password = validated_data.get('password', instance.password)
|
||||||
|
|
||||||
|
if 'new_password' in validated_data and \
|
||||||
|
'old_password' in validated_data:
|
||||||
|
if not validated_data['new_password']:
|
||||||
|
raise serializers.ValidationError({'new_password': 'not found'})
|
||||||
|
|
||||||
|
if not validated_data['old_password']:
|
||||||
|
raise serializers.ValidationError({'old_password': 'not found'})
|
||||||
|
|
||||||
|
if not instance.check_password(validated_data['old_password']):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{'old_password': 'wrong password'})
|
||||||
|
|
||||||
|
if validated_data['new_password'] and \
|
||||||
|
instance.check_password(validated_data['old_password']):
|
||||||
|
# instance.password = validated_data['new_password']
|
||||||
|
instance.set_password(validated_data['new_password'])
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
elif 'email' in validated_data:
|
||||||
|
instance.email = validated_data['email']
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class GuiSettingsSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GuiSettings
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def get_fields(self, *args, **kwargs):
|
||||||
|
fields = super().get_fields(*args, **kwargs)
|
||||||
|
request = self.context.get('request')
|
||||||
|
if request is not None and not request.parser_context.get('kwargs'):
|
||||||
|
fields.pop('id', None)
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
class MessengerSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = MessengePresets
|
||||||
|
fields = '__all__'
|
10
ffplayout/apps/api_player/settings.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(os.path.join(__file__)))
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
|
}
|
||||||
|
}
|
3
ffplayout/apps/api_player/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
26
ffplayout/apps/api_player/urls.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from django.urls import include, path, re_path
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register(r'user/users', views.UserViewSet)
|
||||||
|
router.register(r'guisettings', views.GuiSettingsViewSet, 'guisettings')
|
||||||
|
router.register(r'messenger', views.MessengerViewSet, 'messenger')
|
||||||
|
|
||||||
|
app_name = 'api_player'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('player/', include(router.urls)),
|
||||||
|
path('player/config/', views.Config.as_view()),
|
||||||
|
path('player/log/', views.LogReader.as_view()),
|
||||||
|
path('player/media/', views.Media.as_view()),
|
||||||
|
path('player/media/op/', views.FileOperations.as_view()),
|
||||||
|
re_path(r'^player/media/upload/(?P<filename>[^/]+)$',
|
||||||
|
views.FileUpload.as_view()),
|
||||||
|
path('player/messenger/send/', views.MessegeSender.as_view()),
|
||||||
|
path('player/playlist/', views.Playlist.as_view()),
|
||||||
|
path('player/stats/', views.Statistics.as_view()),
|
||||||
|
path('player/user/current/', views.CurrentUserView.as_view()),
|
||||||
|
path('player/system/', views.SystemCtl.as_view()),
|
||||||
|
]
|
293
ffplayout/apps/api_player/utils.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from platform import uname
|
||||||
|
from subprocess import PIPE, STDOUT, run
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import zmq
|
||||||
|
from apps.api_player.models import GuiSettings
|
||||||
|
from django.conf import settings
|
||||||
|
from natsort import natsorted
|
||||||
|
from pymediainfo import MediaInfo
|
||||||
|
|
||||||
|
|
||||||
|
def read_yaml():
|
||||||
|
config = GuiSettings.objects.filter(id=1).values()[0]
|
||||||
|
|
||||||
|
if config and os.path.isfile(config['playout_config']):
|
||||||
|
with open(config['playout_config'], 'r') as config_file:
|
||||||
|
return yaml.safe_load(config_file)
|
||||||
|
|
||||||
|
|
||||||
|
def write_yaml(data):
|
||||||
|
config = GuiSettings.objects.filter(id=1).values()[0]
|
||||||
|
|
||||||
|
if os.path.isfile(config['playout_config']):
|
||||||
|
with open(config['playout_config'], 'w') as outfile:
|
||||||
|
yaml.dump(data, outfile, default_flow_style=False,
|
||||||
|
sort_keys=False, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(date):
|
||||||
|
config = read_yaml()['playlist']['path']
|
||||||
|
y, m, d = date.split('-')
|
||||||
|
input = os.path.join(config, y, m, '{}.json'.format(date))
|
||||||
|
if os.path.isfile(input):
|
||||||
|
with open(input, 'r') as playlist:
|
||||||
|
return json.load(playlist)
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(data):
|
||||||
|
config = read_yaml()['playlist']['path']
|
||||||
|
y, m, d = data['date'].split('-')
|
||||||
|
output = os.path.join(config, y, m, '{}.json'.format(data['date']))
|
||||||
|
with open(output, "w") as outfile:
|
||||||
|
json.dump(data, outfile, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def read_log(type):
|
||||||
|
config = read_yaml()
|
||||||
|
log_path = config['logging']['log_path']
|
||||||
|
log_file = os.path.join(log_path, '{}.log'.format(type))
|
||||||
|
|
||||||
|
if os.path.isfile(log_file):
|
||||||
|
with open(log_file, 'r') as log:
|
||||||
|
return log.read().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(data):
|
||||||
|
config = read_yaml()
|
||||||
|
address, port = config['text']['bind_address'].split(':')
|
||||||
|
|
||||||
|
context = zmq.Context(1)
|
||||||
|
client = context.socket(zmq.REQ)
|
||||||
|
client.connect('tcp://{}:{}'.format(address, port))
|
||||||
|
|
||||||
|
poll = zmq.Poller()
|
||||||
|
poll.register(client, zmq.POLLIN)
|
||||||
|
|
||||||
|
request = ''
|
||||||
|
reply_msg = ''
|
||||||
|
|
||||||
|
for key, value in data.items():
|
||||||
|
request += "{}='{}':".format(key, value)
|
||||||
|
|
||||||
|
request = "{} reinit {}".format(settings.DRAW_TEXT_NODE, request.rstrip(':'))
|
||||||
|
|
||||||
|
client.send_string(request)
|
||||||
|
|
||||||
|
socks = dict(poll.poll(settings.REQUEST_TIMEOUT))
|
||||||
|
|
||||||
|
if socks.get(client) == zmq.POLLIN:
|
||||||
|
reply = client.recv()
|
||||||
|
|
||||||
|
if reply and reply.decode() == '0 Success':
|
||||||
|
reply_msg = reply.decode()
|
||||||
|
else:
|
||||||
|
reply_msg = reply.decode()
|
||||||
|
else:
|
||||||
|
reply_msg = 'No response from server'
|
||||||
|
|
||||||
|
client.setsockopt(zmq.LINGER, 0)
|
||||||
|
client.close()
|
||||||
|
poll.unregister(client)
|
||||||
|
|
||||||
|
context.term()
|
||||||
|
return {'Success': reply_msg}
|
||||||
|
|
||||||
|
|
||||||
|
def sizeof_fmt(num, suffix='B'):
|
||||||
|
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||||
|
if abs(num) < 1024.0:
|
||||||
|
return "%3.1f%s%s" % (num, unit, suffix)
|
||||||
|
num /= 1024.0
|
||||||
|
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||||
|
|
||||||
|
|
||||||
|
class PlayoutService:
|
||||||
|
def __init__(self):
|
||||||
|
self.service = ['ffplayout-engine.service']
|
||||||
|
self.cmd = ['sudo', '/bin/systemctl']
|
||||||
|
self.proc = None
|
||||||
|
|
||||||
|
def run_cmd(self):
|
||||||
|
self.proc = run(self.cmd + self.service, stdout=PIPE, stderr=STDOUT,
|
||||||
|
encoding="utf-8").stdout
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.cmd.append('start')
|
||||||
|
self.run_cmd()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.cmd.append('stop')
|
||||||
|
self.run_cmd()
|
||||||
|
|
||||||
|
def reload(self):
|
||||||
|
self.cmd.append('reload')
|
||||||
|
self.run_cmd()
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
self.cmd.append('restart')
|
||||||
|
self.run_cmd()
|
||||||
|
|
||||||
|
def status(self):
|
||||||
|
self.cmd.append('is-active')
|
||||||
|
self.run_cmd()
|
||||||
|
|
||||||
|
return self.proc.replace('\n', '')
|
||||||
|
|
||||||
|
def log(self):
|
||||||
|
self.cmd = ['sudo', '/bin/journalctl', '-n', '1000', '-u']
|
||||||
|
|
||||||
|
self.run_cmd()
|
||||||
|
|
||||||
|
return self.proc
|
||||||
|
|
||||||
|
|
||||||
|
class SystemStats:
|
||||||
|
def __init__(self):
|
||||||
|
self.config = GuiSettings.objects.filter(id=1).values()[0]
|
||||||
|
|
||||||
|
def all(self):
|
||||||
|
return {
|
||||||
|
**self.system(),
|
||||||
|
**self.cpu(), **self.ram(), **self.swap(),
|
||||||
|
**self.disk(), **self.net(), **self.net_speed()
|
||||||
|
}
|
||||||
|
|
||||||
|
def system(self):
|
||||||
|
return {
|
||||||
|
'system': uname().system,
|
||||||
|
'node': uname().node,
|
||||||
|
'machine': uname().machine
|
||||||
|
}
|
||||||
|
|
||||||
|
def cpu(self):
|
||||||
|
return {
|
||||||
|
'cpu_usage': psutil.cpu_percent(interval=1),
|
||||||
|
'cpu_load': list(psutil.getloadavg())
|
||||||
|
}
|
||||||
|
|
||||||
|
def ram(self):
|
||||||
|
mem = psutil.virtual_memory()
|
||||||
|
return {
|
||||||
|
'ram_total': [mem.total, sizeof_fmt(mem.total)],
|
||||||
|
'ram_used': [mem.used, sizeof_fmt(mem.used)],
|
||||||
|
'ram_free': [mem.free, sizeof_fmt(mem.free)],
|
||||||
|
'ram_cached': [mem.cached, sizeof_fmt(mem.cached)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def swap(self):
|
||||||
|
swap = psutil.swap_memory()
|
||||||
|
return {
|
||||||
|
'swap_total': [swap.total, sizeof_fmt(swap.total)],
|
||||||
|
'swap_used': [swap.used, sizeof_fmt(swap.used)],
|
||||||
|
'swap_free': [swap.free, sizeof_fmt(swap.free)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def disk(self):
|
||||||
|
root = psutil.disk_usage(self.config['media_disk'])
|
||||||
|
return {
|
||||||
|
'disk_total': [root.total, sizeof_fmt(root.total)],
|
||||||
|
'disk_used': [root.used, sizeof_fmt(root.used)],
|
||||||
|
'disk_free': [root.free, sizeof_fmt(root.free)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def net(self):
|
||||||
|
net = psutil.net_io_counters()
|
||||||
|
return {
|
||||||
|
'net_send': [net.bytes_sent, sizeof_fmt(net.bytes_sent)],
|
||||||
|
'net_recv': [net.bytes_recv, sizeof_fmt(net.bytes_recv)],
|
||||||
|
'net_errin': net.errin,
|
||||||
|
'net_errout': net.errout
|
||||||
|
}
|
||||||
|
|
||||||
|
def net_speed(self):
|
||||||
|
net = psutil.net_if_stats()
|
||||||
|
|
||||||
|
if self.config['net_interface'] not in net:
|
||||||
|
return {
|
||||||
|
'net_speed_send': 'no network interface set!',
|
||||||
|
'net_speed_recv': 'no network interface set!'
|
||||||
|
}
|
||||||
|
|
||||||
|
net = psutil.net_io_counters(pernic=True)[self.config['net_interface']]
|
||||||
|
|
||||||
|
send_start = net.bytes_sent
|
||||||
|
recv_start = net.bytes_recv
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
net = psutil.net_io_counters(pernic=True)[self.config['net_interface']]
|
||||||
|
|
||||||
|
send_end = net.bytes_sent
|
||||||
|
recv_end = net.bytes_recv
|
||||||
|
|
||||||
|
send_sec = send_end - send_start
|
||||||
|
recv_sec = recv_end - recv_start
|
||||||
|
|
||||||
|
return {
|
||||||
|
'net_speed_send': [send_sec, sizeof_fmt(send_sec)],
|
||||||
|
'net_speed_recv': [recv_sec, sizeof_fmt(recv_sec)]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_media_path(extensions, dir=None):
|
||||||
|
config = read_yaml()
|
||||||
|
extensions = extensions.split(' ')
|
||||||
|
playout_extensions = config['storage']['extensions']
|
||||||
|
gui_extensions = [x for x in extensions if x not in playout_extensions]
|
||||||
|
media_path = config['storage']['path'].replace('\\', '/').rstrip('/')
|
||||||
|
media_dir = media_path.split('/')[-1]
|
||||||
|
media_root = os.path.dirname(media_path)
|
||||||
|
if not dir:
|
||||||
|
dir = media_path
|
||||||
|
else:
|
||||||
|
if '/..' in dir:
|
||||||
|
# remove last folder to navigate in upper directory
|
||||||
|
dir = '/'.join(dir.split('/')[:-2])
|
||||||
|
|
||||||
|
dir = dir.lstrip('/')
|
||||||
|
|
||||||
|
if dir.startswith(media_dir):
|
||||||
|
dir = dir[len(media_dir):]
|
||||||
|
|
||||||
|
dir = os.path.join(
|
||||||
|
media_root, media_dir, os.path.abspath('/' + dir).strip('/'))
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(dir, topdown=True):
|
||||||
|
root = root.rstrip('/')
|
||||||
|
media_files = []
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
ext = os.path.splitext(file)[1]
|
||||||
|
if ext in playout_extensions:
|
||||||
|
media_info = MediaInfo.parse(os.path.join(root, file))
|
||||||
|
duration = 0
|
||||||
|
for track in media_info.tracks:
|
||||||
|
if track.track_type == 'General':
|
||||||
|
try:
|
||||||
|
duration = float(
|
||||||
|
track.to_data()["duration"]) / 1000
|
||||||
|
break
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
media_files.append({'file': file, 'duration': duration})
|
||||||
|
elif ext in gui_extensions:
|
||||||
|
media_files.append({'file': file, 'duration': ''})
|
||||||
|
|
||||||
|
dirs = natsorted(dirs)
|
||||||
|
|
||||||
|
if root != media_path:
|
||||||
|
dirs.insert(0, '..')
|
||||||
|
|
||||||
|
if not dirs:
|
||||||
|
dirs = ['..']
|
||||||
|
|
||||||
|
if root.startswith(media_root):
|
||||||
|
root = root[len(media_root):]
|
||||||
|
|
||||||
|
return [root, dirs, natsorted(media_files, key=lambda x: x['file'])]
|
268
ffplayout/apps/api_player/views.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from apps.api_player.models import GuiSettings, MessengePresets
|
||||||
|
from apps.api_player.serializers import (GuiSettingsSerializer, MessengerSerializer,
|
||||||
|
UserSerializer)
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django_filters import rest_framework as filters
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.parsers import FileUploadParser, JSONParser
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from .utils import (PlayoutService, SystemStats, get_media_path, read_json,
|
||||||
|
read_log, read_yaml, send_message, write_json, write_yaml)
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentUserView(APIView):
|
||||||
|
def get(self, request):
|
||||||
|
serializer = UserSerializer(request.user)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class UserFilter(filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['username']
|
||||||
|
|
||||||
|
|
||||||
|
class UserViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
filterset_class = UserFilter
|
||||||
|
|
||||||
|
|
||||||
|
class GuiSettingsViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint that allows media to be viewed.
|
||||||
|
"""
|
||||||
|
queryset = GuiSettings.objects.all()
|
||||||
|
serializer_class = GuiSettingsSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class MessengerFilter(filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MessengePresets
|
||||||
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
|
class MessengerViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = MessengePresets.objects.all()
|
||||||
|
serializer_class = MessengerSerializer
|
||||||
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
filterset_class = MessengerFilter
|
||||||
|
|
||||||
|
|
||||||
|
class MessegeSender(APIView):
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if 'data' in request.data:
|
||||||
|
response = send_message(request.data['data'])
|
||||||
|
return Response({"success": True, 'status': response})
|
||||||
|
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
|
||||||
|
class Config(APIView):
|
||||||
|
"""
|
||||||
|
read and write config from ffplayout engine
|
||||||
|
for reading endpoint is: http://127.0.0.1:8000/api/player/config/?config
|
||||||
|
"""
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if 'configPlayout' in request.GET.dict():
|
||||||
|
yaml_input = read_yaml()
|
||||||
|
|
||||||
|
if yaml_input:
|
||||||
|
return Response(yaml_input)
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
"success": False,
|
||||||
|
"error": "ffpayout engine config file not found!"})
|
||||||
|
else:
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if 'data' in request.data:
|
||||||
|
write_yaml(request.data['data'])
|
||||||
|
return Response({"success": True})
|
||||||
|
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
|
||||||
|
class SystemCtl(APIView):
|
||||||
|
"""
|
||||||
|
controlling the ffplayout-engine systemd services
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if 'run' in request.data:
|
||||||
|
service = PlayoutService()
|
||||||
|
|
||||||
|
if request.data['run'] == 'start':
|
||||||
|
service.start()
|
||||||
|
return Response({"success": True})
|
||||||
|
elif request.data['run'] == 'stop':
|
||||||
|
service.stop()
|
||||||
|
return Response({"success": True})
|
||||||
|
elif request.data['run'] == 'reload':
|
||||||
|
service.reload()
|
||||||
|
return Response({"success": True})
|
||||||
|
elif request.data['run'] == 'restart':
|
||||||
|
service.restart()
|
||||||
|
return Response({"success": True})
|
||||||
|
elif request.data['run'] == 'status':
|
||||||
|
status = service.status()
|
||||||
|
return Response({"data": status})
|
||||||
|
elif request.data['run'] == 'log':
|
||||||
|
log = service.log()
|
||||||
|
return Response({"data": log})
|
||||||
|
else:
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
|
||||||
|
class LogReader(APIView):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if 'type' in request.GET.dict():
|
||||||
|
type = request.GET.dict()['type']
|
||||||
|
log = read_log(type)
|
||||||
|
|
||||||
|
if log:
|
||||||
|
return Response({'log': log})
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
"success": False,
|
||||||
|
"error": "PLayout log file not found!"})
|
||||||
|
else:
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist(APIView):
|
||||||
|
"""
|
||||||
|
read and write config from ffplayout engine
|
||||||
|
for reading endpoint:
|
||||||
|
http://127.0.0.1:8000/api/player/playlist/?date=2020-04-12
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if 'date' in request.GET.dict():
|
||||||
|
date = request.GET.dict()['date']
|
||||||
|
json_input = read_json(date)
|
||||||
|
|
||||||
|
if json_input:
|
||||||
|
return Response(json_input)
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
"success": False,
|
||||||
|
"error": "Playlist from {} not found!".format(date)})
|
||||||
|
else:
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if 'data' in request.data:
|
||||||
|
write_json(request.data['data'])
|
||||||
|
return Response({"success": True})
|
||||||
|
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
|
||||||
|
class Statistics(APIView):
|
||||||
|
"""
|
||||||
|
get system statistics: cpu, ram, etc.
|
||||||
|
for reading, endpoint is: http://127.0.0.1:8000/api/player/stats/?stats=all
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
stats = SystemStats()
|
||||||
|
if 'stats' in request.GET.dict() and request.GET.dict()['stats'] \
|
||||||
|
and hasattr(stats, request.GET.dict()['stats']):
|
||||||
|
return Response(
|
||||||
|
getattr(stats, request.GET.dict()['stats'])())
|
||||||
|
else:
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
|
||||||
|
class Media(APIView):
|
||||||
|
"""
|
||||||
|
get folder/files tree, for building a file explorer
|
||||||
|
for reading, endpoint is: http://127.0.0.1:8000/api/player/media/?path
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if 'extensions' in request.GET.dict():
|
||||||
|
extensions = request.GET.dict()['extensions']
|
||||||
|
|
||||||
|
if 'path' in request.GET.dict() and request.GET.dict()['path']:
|
||||||
|
return Response({'tree': get_media_path(
|
||||||
|
extensions, request.GET.dict()['path']
|
||||||
|
)})
|
||||||
|
elif 'path' in request.GET.dict():
|
||||||
|
return Response({'tree': get_media_path(extensions)})
|
||||||
|
else:
|
||||||
|
return Response({"success": False})
|
||||||
|
else:
|
||||||
|
return Response({"success": False})
|
||||||
|
|
||||||
|
|
||||||
|
class FileUpload(APIView):
|
||||||
|
parser_classes = [FileUploadParser]
|
||||||
|
|
||||||
|
def put(self, request, filename, format=None):
|
||||||
|
root = read_yaml()['storage']['path']
|
||||||
|
file_obj = request.data['file']
|
||||||
|
filename = unquote(filename)
|
||||||
|
path = unquote(request.query_params['path']).split('/')[1:]
|
||||||
|
|
||||||
|
with open(os.path.join(root, *path, filename), 'wb') as outfile:
|
||||||
|
for chunk in file_obj.chunks():
|
||||||
|
outfile.write(chunk)
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
|
||||||
|
class FileOperations(APIView):
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
if 'file' in request.GET.dict() and 'path' in request.GET.dict():
|
||||||
|
root = read_yaml()['storage']['path']
|
||||||
|
_file = request.GET.dict()['file']
|
||||||
|
_path = os.path.join(
|
||||||
|
*(request.GET.dict()['path'].split(os.path.sep)[2:]))
|
||||||
|
fullPath = os.path.join(root, _path)
|
||||||
|
|
||||||
|
if not _file or _file == 'null':
|
||||||
|
if os.path.isdir(fullPath):
|
||||||
|
shutil.rmtree(fullPath, ignore_errors=True)
|
||||||
|
return Response(status=200)
|
||||||
|
else:
|
||||||
|
Response(status=404)
|
||||||
|
elif os.path.isfile(os.path.join(fullPath, _file)):
|
||||||
|
os.remove(os.path.join(fullPath, _file))
|
||||||
|
return Response(status=200)
|
||||||
|
else:
|
||||||
|
Response(status=404)
|
||||||
|
else:
|
||||||
|
return Response(status=404)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if 'folder' in request.data and 'path' in request.data:
|
||||||
|
root = read_yaml()['storage']['path']
|
||||||
|
folder = request.data['folder']
|
||||||
|
_path = request.data['path'].split(os.path.sep)
|
||||||
|
_path = '' if len(_path) == 1 else os.path.join(*_path[1:])
|
||||||
|
fullPath = os.path.join(root, _path, folder)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.mkdir(fullPath)
|
||||||
|
return Response(status=200)
|
||||||
|
except OSError:
|
||||||
|
Response(status=500)
|
||||||
|
else:
|
||||||
|
return Response(status=404)
|
0
ffplayout/ffplayout/__init__.py
Normal file
17
ffplayout/ffplayout/asgi.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for ffplayout project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault(
|
||||||
|
'DJANGO_SETTINGS_MODULE', 'ffplayout.settings.development')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
0
ffplayout/ffplayout/settings/__init__.py
Normal file
141
ffplayout/ffplayout/settings/common.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
"""
|
||||||
|
Django settings for ffplayout project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 3.0.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
from pydoc import locate
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
BASE_DIR = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))))
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'django_filters',
|
||||||
|
'rest_framework',
|
||||||
|
'corsheaders'
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'ffplayout.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'ffplayout.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# simple JWT auth settings
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
|
||||||
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# dynamic app loader
|
||||||
|
APPS_DIR = os.path.join(BASE_DIR, 'apps/')
|
||||||
|
|
||||||
|
for dir in os.listdir(APPS_DIR):
|
||||||
|
if os.path.isdir(os.path.join(APPS_DIR, dir)):
|
||||||
|
app_name = 'apps.{}'.format(dir)
|
||||||
|
|
||||||
|
if app_name not in INSTALLED_APPS:
|
||||||
|
# add app to installed apps
|
||||||
|
INSTALLED_APPS += (app_name, )
|
||||||
|
|
||||||
|
if os.path.isfile(os.path.join(APPS_DIR, dir, 'settings.py')):
|
||||||
|
db = locate('{}.settings.DATABASES'.format(app_name))
|
||||||
|
|
||||||
|
for key in db:
|
||||||
|
if key not in DATABASES:
|
||||||
|
# add app db to DATABASES
|
||||||
|
DATABASES.update({key: db[key]})
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = '/var/www/ffplayout/ffplayout/static/'
|
||||||
|
|
||||||
|
# ffmpeg filter node, needs to be edit only when the filter chain changes
|
||||||
|
DRAW_TEXT_NODE = 'Parsed_drawtext_2'
|
||||||
|
|
||||||
|
# zmq settings
|
||||||
|
REQUEST_TIMEOUT = 1000
|
26
ffplayout/ffplayout/settings/development.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from ffplayout.settings.common import *
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'dhgfk(gl&16krnt_7*dp(9b3w*ft%nbsg-h2)&ihbte4le#o4f'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
# REST API
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
],
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CORS_ORIGIN_WHITELIST = (
|
||||||
|
'http://localhost:3000',
|
||||||
|
'http://localhost:8000',
|
||||||
|
'http://ffplayout.local'
|
||||||
|
)
|
25
ffplayout/ffplayout/settings/production.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from ffplayout.settings.common import *
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = '---a-very-important-secret-key:-generate-it-new---'
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['localhost']
|
||||||
|
|
||||||
|
# REST API
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
],
|
||||||
|
'DEFAULT_RENDERER_CLASSES': (
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CORS_ORIGIN_WHITELIST = (
|
||||||
|
'http://ffplayout.local',
|
||||||
|
)
|
44
ffplayout/ffplayout/urls.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""ffplayout URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/3.0/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.conf import settings
|
||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework_simplejwt.views import (TokenObtainPairView,
|
||||||
|
TokenRefreshView)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('api-auth/', include(
|
||||||
|
'rest_framework.urls', namespace='rest_framework')),
|
||||||
|
path('auth/token/', TokenObtainPairView.as_view(),
|
||||||
|
name='token_obtain_pair'),
|
||||||
|
path('auth/token/refresh/', TokenRefreshView.as_view(),
|
||||||
|
name='token_refresh')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# dynamic url loader
|
||||||
|
for dir in os.listdir(settings.APPS_DIR):
|
||||||
|
if os.path.isdir(os.path.join(settings.APPS_DIR, dir)):
|
||||||
|
app_name = 'apps.{}'.format(dir)
|
||||||
|
|
||||||
|
_path = path('api/', include(
|
||||||
|
'{}.urls'.format(app_name),
|
||||||
|
namespace='{}'.format(app_name.split('.')[1])))
|
||||||
|
|
||||||
|
if _path not in urlpatterns:
|
||||||
|
urlpatterns += (_path, )
|
17
ffplayout/ffplayout/wsgi.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for ffplayout project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault(
|
||||||
|
'DJANGO_SETTINGS_MODULE', 'ffplayout.settings.development')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
13
ffplayout/frontend/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
23
ffplayout/frontend/.eslintrc.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint'
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'@nuxtjs',
|
||||||
|
'plugin:nuxt/recommended'
|
||||||
|
],
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
'vue/html-indent': ['error', 4],
|
||||||
|
'vue/html-closing-bracket-newline': 'off',
|
||||||
|
'indent': [2, 4],
|
||||||
|
'no-tabs': 'off',
|
||||||
|
"no-console": 0,
|
||||||
|
"camelcase": ["error", {properties: "never"}]
|
||||||
|
}
|
||||||
|
}
|
90
ffplayout/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Node template
|
||||||
|
# Logs
|
||||||
|
/logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# Nuxt generate
|
||||||
|
dist
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Service worker
|
||||||
|
sw.*
|
||||||
|
|
||||||
|
# Mac OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.swp
|
27
ffplayout/frontend/README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# ffplayout
|
||||||
|
|
||||||
|
> web GUI for ffplayout engine
|
||||||
|
|
||||||
|
## Build Setup
|
||||||
|
|
||||||
|
create `.env`file with `API_URL`, for example:
|
||||||
|
```
|
||||||
|
API_URL="http://localhost:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# install dependencies
|
||||||
|
$ npm run install
|
||||||
|
|
||||||
|
# serve with hot reload at localhost:3000
|
||||||
|
$ npm run dev
|
||||||
|
|
||||||
|
# build for production and launch server
|
||||||
|
$ npm run build
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# generate static project
|
||||||
|
$ npm run generate
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).
|
7
ffplayout/frontend/assets/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# ASSETS
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
|
497
ffplayout/frontend/assets/css/_bootswatch.scss
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
// Slate 4.3.1
|
||||||
|
// Bootswatch
|
||||||
|
|
||||||
|
|
||||||
|
// Variables ===================================================================
|
||||||
|
|
||||||
|
@mixin btn-shadow($color){
|
||||||
|
@include gradient-y-three-colors(lighten($color, 6%), $color, 60%, darken($color, 4%));
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin btn-shadow-inverse($color){
|
||||||
|
@include gradient-y-three-colors(darken($color, 18%), darken($color, 15%), 40%, darken($color, 13%));
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navbar ======================================================================
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
border-color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-fixed-top {
|
||||||
|
border-width: 0 0 1px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-fixed-bottom {
|
||||||
|
border-width: 1px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: 1rem;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
@include btn-shadow-inverse($gray-800);
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-brand {
|
||||||
|
padding: 0.75rem 1rem calc(54px - 0.75rem - 30px);
|
||||||
|
margin-right: 0;
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active .nav-link {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-nav .nav-item + .nav-item {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bg-light {
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
@include btn-shadow-inverse($gray-600);
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.navbar-expand-sm {
|
||||||
|
.navbar-brand,
|
||||||
|
.nav-link {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.navbar-expand-md {
|
||||||
|
.navbar-brand,
|
||||||
|
.nav-link {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.navbar-expand-lg {
|
||||||
|
.navbar-brand,
|
||||||
|
.nav-link {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buttons =====================================================================
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-color: rgba(0, 0, 0, 0.6);
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
&:not([disabled]):not(.disabled).active,
|
||||||
|
&.disabled {
|
||||||
|
border-color: rgba(0, 0, 0, 0.6);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:not([disabled]):not(.disabled):active,
|
||||||
|
&:not([disabled]):not(.disabled):active:hover,
|
||||||
|
&:not([disabled]):not(.disabled).active:hover {
|
||||||
|
border-color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@include btn-shadow($primary);
|
||||||
|
|
||||||
|
&:not([disabled]):not(.disabled):hover,
|
||||||
|
&:not([disabled]):not(.disabled):focus,
|
||||||
|
&:not([disabled]):not(.disabled):active:hover,
|
||||||
|
&:not([disabled]):not(.disabled).active:hover {
|
||||||
|
@include btn-shadow-inverse($primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@include btn-shadow($secondary);
|
||||||
|
|
||||||
|
&:not([disabled]):not(.disabled):hover,
|
||||||
|
&:not([disabled]):not(.disabled):focus,
|
||||||
|
&:not([disabled]):not(.disabled):active,
|
||||||
|
&:not([disabled]):not(.disabled).active {
|
||||||
|
@include btn-shadow-inverse($secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
@include btn-shadow($success);
|
||||||
|
color: $white;
|
||||||
|
|
||||||
|
&:not([disabled]):not(.disabled):hover,
|
||||||
|
&:not([disabled]):not(.disabled):focus,
|
||||||
|
&:not([disabled]):not(.disabled):active,
|
||||||
|
&:not([disabled]):not(.disabled).active {
|
||||||
|
@include btn-shadow-inverse($success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
@include btn-shadow($info);
|
||||||
|
color: $white;
|
||||||
|
|
||||||
|
&:not([disabled]):not(.disabled):hover,
|
||||||
|
&:not([disabled]):not(.disabled):focus,
|
||||||
|
&:not([disabled]):not(.disabled):active,
|
||||||
|
&:not([disabled]):not(.disabled).active {
|
||||||
|
@include btn-shadow-inverse($info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
@include btn-shadow($warning);
|
||||||
|
color: $white;
|
||||||
|
|
||||||
|
&:not([disabled]):not(.disabled):hover,
|
||||||
|
&:not([disabled]):not(.disabled):focus,
|
||||||
|
&:not([disabled]):not(.disabled):active,
|
||||||
|
&:not([disabled]):not(.disabled).active {
|
||||||
|
@include btn-shadow-inverse($warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@include btn-shadow($danger);
|
||||||
|
|
||||||
|
&:not([disabled]):not(.disabled):hover,
|
||||||
|
&:not([disabled]):not(.disabled):focus,
|
||||||
|
&:not([disabled]):not(.disabled):active,
|
||||||
|
&:not([disabled]):not(.disabled).active {
|
||||||
|
@include btn-shadow-inverse($danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link,
|
||||||
|
.btn-link:hover {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group,
|
||||||
|
.btn-group-vertical {
|
||||||
|
|
||||||
|
.btn.active {
|
||||||
|
border-color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typography ==================================================================
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tables ======================================================================
|
||||||
|
|
||||||
|
.table {
|
||||||
|
|
||||||
|
&-primary,
|
||||||
|
&-secondary,
|
||||||
|
&-success,
|
||||||
|
&-info,
|
||||||
|
&-warning,
|
||||||
|
&-danger {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-primary {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-secondary {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-light {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-dark {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-success {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-info {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-danger {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-warning {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-active {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $table-active-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-hover {
|
||||||
|
|
||||||
|
.table-primary:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: darken($primary, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-secondary:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: darken($secondary, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-light:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: darken($light, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: darken($dark, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-success:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: darken($success, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-info:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: darken($info, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-danger:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: darken($danger, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-warning:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: darken($warning, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-active:hover {
|
||||||
|
&, > th, > td {
|
||||||
|
background-color: $table-active-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forms =======================================================================
|
||||||
|
|
||||||
|
legend {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-addon {
|
||||||
|
@include btn-shadow($secondary);
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navs ========================================================================
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
@include btn-shadow-inverse($gray-800);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
|
&:not([disabled]):not(.disabled):hover,
|
||||||
|
&:not([disabled]):not(.disabled):focus,
|
||||||
|
&:not([disabled]):not(.disabled):active,
|
||||||
|
&:not([disabled]):not(.disabled).active {
|
||||||
|
@include btn-shadow($gray-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link,
|
||||||
|
.nav-link:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-pills {
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
@include btn-shadow($gray-800);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@include btn-shadow-inverse($gray-800);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active,
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
@include btn-shadow-inverse($gray-800);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.disabled,
|
||||||
|
.nav-link.disabled:hover {
|
||||||
|
@include btn-shadow($gray-800);
|
||||||
|
color: $nav-link-disabled-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
@include btn-shadow($gray-800);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@include btn-shadow-inverse($gray-800);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.active .page-link {
|
||||||
|
@include btn-shadow-inverse($gray-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.disabled .page-link {
|
||||||
|
@include btn-shadow($gray-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
background-color: transparent;
|
||||||
|
@include btn-shadow($gray-800);
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicators ==================================================================
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: $close-color;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
color: $white;
|
||||||
|
|
||||||
|
a,
|
||||||
|
.alert-link {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $color, $value in $theme-colors {
|
||||||
|
&-#{$color} {
|
||||||
|
background-color: $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-light {
|
||||||
|
&,
|
||||||
|
& a:not(.btn),
|
||||||
|
& .alert-link {
|
||||||
|
color: $body-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
|
||||||
|
&-success,
|
||||||
|
&-warning,
|
||||||
|
&-info {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress bars ===============================================================
|
||||||
|
|
||||||
|
// Containers ==================================================================
|
||||||
|
|
||||||
|
.jumbotron {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
|
||||||
|
&-item:hover {
|
||||||
|
background-color: darken($gray-900, 5%);
|
||||||
|
}
|
||||||
|
}
|
164
ffplayout/frontend/assets/css/_variables.scss
Executable file
@ -0,0 +1,164 @@
|
|||||||
|
// Slate 4.3.1
|
||||||
|
// Bootswatch
|
||||||
|
|
||||||
|
//
|
||||||
|
// Color system
|
||||||
|
//
|
||||||
|
|
||||||
|
$white: #fff !default;
|
||||||
|
$gray-100: #f8f9fa !default;
|
||||||
|
$gray-200: #e9ecef !default;
|
||||||
|
$gray-300: #dee2e6 !default;
|
||||||
|
$gray-400: #ced4da !default;
|
||||||
|
$gray-500: #999 !default;
|
||||||
|
$gray-600: #7A8288 !default;
|
||||||
|
$gray-700: #52575C !default;
|
||||||
|
$gray-800: #3A3F44 !default;
|
||||||
|
$gray-900: #272B30 !default;
|
||||||
|
$black: #000 !default;
|
||||||
|
|
||||||
|
$blue: #007bff !default;
|
||||||
|
$indigo: #6610f2 !default;
|
||||||
|
$purple: #6f42c1 !default;
|
||||||
|
$pink: #e83e8c !default;
|
||||||
|
$red: #ee5f5b !default;
|
||||||
|
$orange: #fd7e14 !default;
|
||||||
|
$yellow: #f89406 !default;
|
||||||
|
$green: #62c462 !default;
|
||||||
|
$teal: #20c997 !default;
|
||||||
|
$cyan: #5bc0de !default;
|
||||||
|
|
||||||
|
$primary: $gray-800 !default;
|
||||||
|
$secondary: $gray-600 !default;
|
||||||
|
$success: $green !default;
|
||||||
|
$info: $cyan !default;
|
||||||
|
$warning: $yellow !default;
|
||||||
|
$danger: $red !default;
|
||||||
|
$light: $gray-200 !default;
|
||||||
|
$dark: $gray-900 !default;
|
||||||
|
|
||||||
|
$yiq-contrasted-threshold: 170 !default;
|
||||||
|
|
||||||
|
// Body
|
||||||
|
|
||||||
|
$body-bg: $gray-900 !default;
|
||||||
|
$body-color: #aaa !default;
|
||||||
|
|
||||||
|
// Links
|
||||||
|
|
||||||
|
$link-color: $white !default;
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
|
||||||
|
$font-size-base: 0.9375rem !default;
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
|
||||||
|
$table-color: $white !default;
|
||||||
|
$table-accent-bg: rgba($white,.05) !default;
|
||||||
|
$table-hover-bg: rgba($white,.075) !default;
|
||||||
|
|
||||||
|
$table-border-color: rgba($black,.6) !default;
|
||||||
|
|
||||||
|
$table-dark-border-color: $table-border-color !default;
|
||||||
|
$table-dark-color: $white !default;
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
|
||||||
|
$input-btn-padding-y: .75rem !default;
|
||||||
|
$input-btn-padding-x: 1rem !default;
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
|
||||||
|
$input-disabled-bg: #ccc !default;
|
||||||
|
|
||||||
|
// Dropdowns
|
||||||
|
|
||||||
|
$dropdown-bg: $gray-800 !default;
|
||||||
|
$dropdown-border-color: rgba($black, .6) !default;
|
||||||
|
$dropdown-divider-bg: rgba($black,.15) !default;
|
||||||
|
|
||||||
|
$dropdown-link-color: $body-color !default;
|
||||||
|
$dropdown-link-hover-color: $white !default;
|
||||||
|
$dropdown-link-hover-bg: $body-bg !default;
|
||||||
|
|
||||||
|
// Navs
|
||||||
|
|
||||||
|
$nav-tabs-border-color: rgba($black, 0.6) !default;
|
||||||
|
$nav-tabs-link-hover-border-color: $nav-tabs-border-color !default;
|
||||||
|
$nav-tabs-link-active-color: $white !default;
|
||||||
|
$nav-tabs-link-active-border-color: $nav-tabs-border-color !default;
|
||||||
|
|
||||||
|
// Navbar
|
||||||
|
|
||||||
|
$navbar-padding-y: 0 !default;
|
||||||
|
|
||||||
|
$navbar-dark-hover-color: $white !default;
|
||||||
|
|
||||||
|
$navbar-light-hover-color: $gray-800 !default;
|
||||||
|
$navbar-light-active-color: $gray-800 !default;
|
||||||
|
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
|
||||||
|
$pagination-color: $white !default;
|
||||||
|
$pagination-bg: transparent !default;
|
||||||
|
$pagination-border-color: rgba($black, 0.6) !default;
|
||||||
|
|
||||||
|
$pagination-hover-color: $white !default;
|
||||||
|
$pagination-hover-bg: transparent !default;
|
||||||
|
$pagination-hover-border-color: rgba($black, 0.6) !default;
|
||||||
|
|
||||||
|
$pagination-active-bg: transparent !default;
|
||||||
|
$pagination-active-border-color: rgba($black, 0.6) !default;
|
||||||
|
|
||||||
|
$pagination-disabled-bg: transparent !default;
|
||||||
|
$pagination-disabled-border-color: rgba($black, 0.6) !default;
|
||||||
|
|
||||||
|
|
||||||
|
// Jumbotron
|
||||||
|
|
||||||
|
$jumbotron-bg: darken($gray-900, 5%) !default;
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
|
||||||
|
$card-border-color: rgba($black, 0.6) !default;
|
||||||
|
$card-cap-bg: lighten($gray-800, 10%) !default;
|
||||||
|
$card-bg: lighten($body-bg, 5%) !default;
|
||||||
|
|
||||||
|
// Popovers
|
||||||
|
|
||||||
|
$popover-bg: lighten($body-bg, 5%) !default;
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
|
||||||
|
$modal-content-bg: lighten($body-bg, 5%) !default;
|
||||||
|
|
||||||
|
$modal-header-border-color: rgba(0,0,0,.2) !default;
|
||||||
|
|
||||||
|
// Progress bars
|
||||||
|
|
||||||
|
$progress-bg: darken($gray-900, 5%) !default;
|
||||||
|
$progress-bar-color: $gray-600 !default;
|
||||||
|
|
||||||
|
// List group
|
||||||
|
|
||||||
|
$list-group-bg: lighten($body-bg, 5%) !default;
|
||||||
|
$list-group-border-color: rgba($black, 0.6) !default;
|
||||||
|
|
||||||
|
$list-group-hover-bg: lighten($body-bg, 10%) !default;
|
||||||
|
$list-group-active-color: $white !default;
|
||||||
|
$list-group-active-bg: $list-group-hover-bg !default;
|
||||||
|
$list-group-active-border-color: $list-group-border-color !default;
|
||||||
|
|
||||||
|
$list-group-disabled-color: $gray-700 !default;
|
||||||
|
|
||||||
|
$list-group-action-color: $white !default;
|
||||||
|
|
||||||
|
// Breadcrumbs
|
||||||
|
|
||||||
|
$breadcrumb-active-color: $gray-500 !default;
|
||||||
|
|
||||||
|
// Code
|
||||||
|
|
||||||
|
$pre-color: inherit !default;
|
12
ffplayout/frontend/assets/css/bootstrap.min.css
vendored
Normal file
BIN
ffplayout/frontend/assets/fonts/DigitalNumbers-Regular.woff2
Normal file
93
ffplayout/frontend/assets/fonts/font-license.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright (c) 2015, Stephan Ahlf (https://github.com/s-a/digital-numbers-font stephan.ahlf@googlemail.com)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
156
ffplayout/frontend/assets/scss/globals.scss
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
#__nuxt, #__layout, #__layout > div, #__layout > div > div {
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family: "DigitalNumbers-Regular";
|
||||||
|
src: url("~@/assets/fonts/DigitalNumbers-Regular.woff2") format("woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-item {
|
||||||
|
background: none;
|
||||||
|
padding: .1em;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-icons {
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
background-color: #32383E;
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
float: right;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control, .tags-list ul li div input, .custom-select, .custom-control-label::before {
|
||||||
|
color: #e4e4e4;
|
||||||
|
background-color: #32383E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:disabled, .form-control[readonly] {
|
||||||
|
color: #b4b4b4;
|
||||||
|
background-color:#485159;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
background: #32383E url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='rgb(228, 228, 228)' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 1rem center/8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus, .form-control.focus, .b-form-tags.focus, .custom-select:focus {
|
||||||
|
color: #f5f5f5;
|
||||||
|
background-color: #424a53;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-file-label {
|
||||||
|
background-color: #424a53;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-file-label::after {
|
||||||
|
background-color: #32383E;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-div {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-div .ps {
|
||||||
|
padding-left: .4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ps__thumb-x {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitpanes__pane {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitpanes.default-theme .splitpanes__pane {
|
||||||
|
background-color: $dark;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, .2) inset;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-theme.splitpanes--vertical > .splitpanes__splitter, .default-theme .splitpanes--vertical > .splitpanes__splitter {
|
||||||
|
border-left: 1px solid $dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitpanes.default-theme .splitpanes__splitter {
|
||||||
|
background-color: $dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitpanes.default-theme .splitpanes__splitter::after, .splitpanes.default-theme .splitpanes__splitter::before {
|
||||||
|
background-color: rgba(136, 136, 136, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ps .ps__rail-x:hover, .ps .ps__rail-y:hover, .ps .ps__rail-x:focus, .ps .ps__rail-y:focus, .ps .ps__rail-x.ps--clicking, .ps .ps__rail-y.ps--clicking {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-icons-col {
|
||||||
|
max-width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-play-col {
|
||||||
|
max-width: 15px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-dur-col {
|
||||||
|
min-width: 95px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-div .ps {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-container .ps {
|
||||||
|
height: 94.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-list {
|
||||||
|
max-height: 93%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-item-text {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 95%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background-color: #ff9c36
|
||||||
|
}
|
||||||
|
|
||||||
|
.grabbing {
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
62
ffplayout/frontend/components/Menu.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="menu">
|
||||||
|
<b-button-group size="sm">
|
||||||
|
<b-button to="/" variant="primary">
|
||||||
|
Home
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/player" variant="primary">
|
||||||
|
Player
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/media" variant="primary">
|
||||||
|
Media
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/message" variant="primary">
|
||||||
|
Message
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/logging" variant="primary">
|
||||||
|
Logging
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/configure" variant="primary">
|
||||||
|
Configure
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/" variant="primary" @click="logout()">
|
||||||
|
Logout
|
||||||
|
</b-button>
|
||||||
|
</b-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Menu',
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async logout () {
|
||||||
|
try {
|
||||||
|
await this.$store.commit('auth/REMOVE_TOKEN')
|
||||||
|
await this.$store.commit('auth/UPDATE_IS_LOGIN', false)
|
||||||
|
} catch (e) {
|
||||||
|
this.formError = e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" >
|
||||||
|
.menu {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0;
|
||||||
|
padding: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu div {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
7
ffplayout/frontend/components/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# COMPONENTS
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
The components directory contains your Vue.js Components.
|
||||||
|
|
||||||
|
_Nuxt.js doesn't supercharge these components._
|
56
ffplayout/frontend/components/VideoPlayer.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="options.sources">
|
||||||
|
<video
|
||||||
|
:id="reference"
|
||||||
|
class="video-js vjs-default-skin vjs-big-play-centered vjs-16-9"
|
||||||
|
width="1024"
|
||||||
|
height="576"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
import videojs from 'video.js'
|
||||||
|
require('video.js/dist/video-js.css')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VideoPlayer',
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reference: {
|
||||||
|
type: String,
|
||||||
|
default () {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
player: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
this.player = videojs(this.reference, this.options, function onPlayerReady () {
|
||||||
|
// console.log('onPlayerReady', this);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy () {
|
||||||
|
if (this.player) {
|
||||||
|
this.player.dispose()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
7
ffplayout/frontend/layouts/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# LAYOUTS
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your Application Layouts.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts).
|
27
ffplayout/frontend/layouts/default.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<nuxt />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||||
|
Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
color: #c4c4c4;
|
||||||
|
font-size: 15px;
|
||||||
|
word-spacing: 1px;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #c4c4c4;
|
||||||
|
}
|
||||||
|
</style>
|
8
ffplayout/frontend/middleware/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# MIDDLEWARE
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your application middleware.
|
||||||
|
Middleware let you define custom functions that can be run before rendering either a page or a group of pages.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
|
7
ffplayout/frontend/middleware/auth.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default async function ({ store, redirect }) {
|
||||||
|
await store.dispatch('auth/inspectToken')
|
||||||
|
|
||||||
|
if (!store.state.auth.isLogin) {
|
||||||
|
return redirect('/')
|
||||||
|
}
|
||||||
|
}
|
104
ffplayout/frontend/nuxt.config.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mode: 'spa',
|
||||||
|
/*
|
||||||
|
** Headers of the page
|
||||||
|
*/
|
||||||
|
head: {
|
||||||
|
title: process.env.npm_package_name || '',
|
||||||
|
meta: [{
|
||||||
|
charset: 'utf-8'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'viewport',
|
||||||
|
content: 'width=device-width, initial-scale=1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'description',
|
||||||
|
name: 'description',
|
||||||
|
content: process.env.npm_package_description || ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
link: [{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
href: '/favicon.ico'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
** Customize the progress-bar color
|
||||||
|
*/
|
||||||
|
loading: {
|
||||||
|
color: '#ff9c36'
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
** Global CSS
|
||||||
|
*/
|
||||||
|
css: [
|
||||||
|
'@/assets/css/bootstrap.min.css'
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
** Plugins to load before mounting the App
|
||||||
|
*/
|
||||||
|
plugins: [
|
||||||
|
{ src: '~/plugins/axios' },
|
||||||
|
{ src: '~/plugins/filters' },
|
||||||
|
{ src: '~plugins/video.js', ssr: false },
|
||||||
|
{ src: '~plugins/scrollbar.js', ssr: false },
|
||||||
|
{ src: '~plugins/splitpanes.js', ssr: false },
|
||||||
|
{ src: '~plugins/loading.js', ssr: false },
|
||||||
|
{ src: '~/plugins/helpers.js' },
|
||||||
|
{ src: '~plugins/draggable.js', ssr: false }
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
** Nuxt.js dev-modules
|
||||||
|
*/
|
||||||
|
buildModules: [
|
||||||
|
// Doc: https://github.com/nuxt-community/eslint-module
|
||||||
|
'@nuxtjs/eslint-module'
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
** Nuxt.js modules
|
||||||
|
*/
|
||||||
|
modules: [
|
||||||
|
// Doc: https://bootstrap-vue.js.org
|
||||||
|
'bootstrap-vue/nuxt',
|
||||||
|
'@nuxtjs/axios',
|
||||||
|
'@nuxtjs/style-resources',
|
||||||
|
// Doc: https://github.com/nuxt-community/dotenv-module
|
||||||
|
'@nuxtjs/dotenv',
|
||||||
|
'cookie-universal-nuxt',
|
||||||
|
'nuxt-dayjs-module'
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Axios module configuration
|
||||||
|
** See https://axios.nuxtjs.org/options
|
||||||
|
*/
|
||||||
|
axios: {
|
||||||
|
baseURL: process.env.API_URL
|
||||||
|
},
|
||||||
|
|
||||||
|
styleResources: {
|
||||||
|
scss: [
|
||||||
|
'@/assets/css/_variables.scss',
|
||||||
|
'@/assets/scss/globals.scss'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
bootstrapVue: {
|
||||||
|
bootstrapCSS: false,
|
||||||
|
icons: true
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Build configuration
|
||||||
|
*/
|
||||||
|
build: {
|
||||||
|
/*
|
||||||
|
** You can extend webpack config here
|
||||||
|
*/
|
||||||
|
extend(config, ctx) {}
|
||||||
|
}
|
||||||
|
}
|
13413
ffplayout/frontend/package-lock.json
generated
Normal file
39
ffplayout/frontend/package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "ffplayout",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "web GUI for ffplayout engine",
|
||||||
|
"author": "Jonathan Baecker",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"start": "nuxt start",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxtjs/axios": "^5.10.3",
|
||||||
|
"@nuxtjs/dotenv": "^1.4.1",
|
||||||
|
"bootstrap": "^4.4.1",
|
||||||
|
"bootstrap-vue": "^2.13.0",
|
||||||
|
"cookie-universal-nuxt": "^2.1.3",
|
||||||
|
"jwt-decode": "^2.2.0",
|
||||||
|
"nuxt": "^2.12.2",
|
||||||
|
"nuxt-dayjs-module": "^1.1.2",
|
||||||
|
"splitpanes": "^2.2.1",
|
||||||
|
"video.js": "^7.7.5",
|
||||||
|
"vue-loading-overlay": "^3.3.2",
|
||||||
|
"vue2-perfect-scrollbar": "^1.5.0",
|
||||||
|
"vuedraggable": "^2.23.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxtjs/eslint-config": "^2.0.2",
|
||||||
|
"@nuxtjs/eslint-module": "^1.2.0",
|
||||||
|
"@nuxtjs/style-resources": "^1.0.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-plugin-nuxt": ">=0.5.2",
|
||||||
|
"node-sass": "^4.14.0",
|
||||||
|
"sass-loader": "^8.0.2"
|
||||||
|
}
|
||||||
|
}
|
6
ffplayout/frontend/pages/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# PAGES
|
||||||
|
|
||||||
|
This directory contains your Application Views and Routes.
|
||||||
|
The framework reads all the `*.vue` files inside this directory and creates the router of your application.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing).
|
332
ffplayout/frontend/pages/configure.vue
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Menu />
|
||||||
|
<b-card no-body>
|
||||||
|
<b-tabs pills card vertical>
|
||||||
|
<b-tab title="GUI" active @click="resetAlert()">
|
||||||
|
<b-container class="config-container">
|
||||||
|
<b-form v-if="configGui" @submit="onSubmitGui">
|
||||||
|
<b-form-group
|
||||||
|
label-cols-lg="2"
|
||||||
|
label="GUI Configuration"
|
||||||
|
label-size="lg"
|
||||||
|
label-class="font-weight-bold pt-0"
|
||||||
|
class="config-group"
|
||||||
|
>
|
||||||
|
<b-form-group
|
||||||
|
v-for="(prop, name, idx) in configGui"
|
||||||
|
:key="idx"
|
||||||
|
label-cols-sm="2"
|
||||||
|
:label="name"
|
||||||
|
label-align-sm="right"
|
||||||
|
:label-for="name"
|
||||||
|
>
|
||||||
|
<b-form-tags
|
||||||
|
v-if="name === 'extra_extensions'"
|
||||||
|
v-model="configGui[name]"
|
||||||
|
:input-id="name"
|
||||||
|
separator=" ,;"
|
||||||
|
:placeholder="`add ${name}...`"
|
||||||
|
class="mb-2 tags-list"
|
||||||
|
/>
|
||||||
|
<b-form-text v-if="name === 'extra_extensions'">
|
||||||
|
Visible extensions only for the GUI and not the playout
|
||||||
|
</b-form-text>
|
||||||
|
<b-form-select v-else-if="name === 'net_interface'" :id="name" v-model="configGui[name]" :options="netChoices" :value="prop" />
|
||||||
|
<b-form-input v-else :id="name" v-model="configGui[name]" :value="prop" />
|
||||||
|
</b-form-group>
|
||||||
|
</b-form-group>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="1" style="min-width: 85px">
|
||||||
|
<b-button type="submit" variant="primary">
|
||||||
|
Save
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-alert v-model="showAlert" :variant="alertVariant" dismissible>
|
||||||
|
{{ alertMsg }}
|
||||||
|
</b-alert>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-form>
|
||||||
|
</b-container>
|
||||||
|
</b-tab>
|
||||||
|
<b-tab title="Playout" @click="resetAlert()">
|
||||||
|
<b-container class="config-container">
|
||||||
|
<b-form v-if="configPlayout" @submit="onSubmitPlayout">
|
||||||
|
<b-form-group
|
||||||
|
v-for="(item, key, index) in configPlayout"
|
||||||
|
:key="index"
|
||||||
|
label-cols-lg="2"
|
||||||
|
:label="key"
|
||||||
|
label-size="lg"
|
||||||
|
label-class="font-weight-bold pt-0"
|
||||||
|
class="config-group"
|
||||||
|
>
|
||||||
|
<b-form-group
|
||||||
|
v-for="(prop, name, idx) in item"
|
||||||
|
:key="idx"
|
||||||
|
label-cols-sm="2"
|
||||||
|
:label="(typeof prop === 'boolean' || name === 'helptext') ? '' : name"
|
||||||
|
label-align-sm="right"
|
||||||
|
:label-for="name"
|
||||||
|
>
|
||||||
|
<b-form-textarea
|
||||||
|
v-if="name === 'helptext'"
|
||||||
|
id="textarea-plaintext"
|
||||||
|
plaintext
|
||||||
|
:value="prop"
|
||||||
|
rows="2"
|
||||||
|
max-rows="8"
|
||||||
|
class="text-area"
|
||||||
|
/>
|
||||||
|
<b-form-checkbox
|
||||||
|
v-else-if="typeof prop === 'boolean'"
|
||||||
|
:id="name"
|
||||||
|
v-model="configPlayout[key][name]"
|
||||||
|
:name="name"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</b-form-checkbox>
|
||||||
|
<b-form-input
|
||||||
|
v-else-if="prop && prop.toString().match(/^-?\d+[.,]\d+$/)"
|
||||||
|
:id="name"
|
||||||
|
v-model="configPlayout[key][name]"
|
||||||
|
type="number"
|
||||||
|
step="0.001"
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
<b-form-input
|
||||||
|
v-else-if="prop && !isNaN(prop)"
|
||||||
|
:id="name"
|
||||||
|
v-model="configPlayout[key][name]"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
<b-form-tags
|
||||||
|
v-else-if="Array.isArray(prop)"
|
||||||
|
v-model="configPlayout[key][name]"
|
||||||
|
:input-id="name"
|
||||||
|
separator=" ,;"
|
||||||
|
:placeholder="`add ${name}...`"
|
||||||
|
class="mb-2 tags-list"
|
||||||
|
/>
|
||||||
|
<b-form-input
|
||||||
|
v-else-if="name.includes('pass')"
|
||||||
|
:id="name"
|
||||||
|
v-model="configPlayout[key][name]"
|
||||||
|
type="password"
|
||||||
|
:value="prop"
|
||||||
|
/>
|
||||||
|
<b-form-input v-else :id="name" v-model="configPlayout[key][name]" :value="prop" />
|
||||||
|
</b-form-group>
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="1" style="min-width: 85px">
|
||||||
|
<b-button type="submit" variant="primary">
|
||||||
|
Save
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-alert v-model="showAlert" :variant="alertVariant" dismissible>
|
||||||
|
{{ alertMsg }}
|
||||||
|
</b-alert>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-form>
|
||||||
|
</b-container>
|
||||||
|
</b-tab>
|
||||||
|
<b-tab title="User" @click="resetAlert()">
|
||||||
|
<b-card-text>
|
||||||
|
<b-container class="config-container">
|
||||||
|
<b-form v-if="configUser" @submit="onSubmitUser">
|
||||||
|
<b-form-group
|
||||||
|
label-cols-lg="2"
|
||||||
|
label="User Configuration"
|
||||||
|
label-size="lg"
|
||||||
|
label-class="font-weight-bold pt-0"
|
||||||
|
class="config-group"
|
||||||
|
>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="2"
|
||||||
|
:label="'username'"
|
||||||
|
label-align-sm="right"
|
||||||
|
:label-for="'username'"
|
||||||
|
>
|
||||||
|
<b-form-input id="username" v-model="configUser['username']" :value="configUser['username']" disabled />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="2"
|
||||||
|
:label="'email'"
|
||||||
|
label-align-sm="right"
|
||||||
|
:label-for="'email'"
|
||||||
|
>
|
||||||
|
<b-form-input id="email" v-model="configUser['email']" :value="configUser['email']" />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="2"
|
||||||
|
label="old password"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="oldPass"
|
||||||
|
>
|
||||||
|
<b-form-input id="oldPass" v-model="oldPass" type="password" />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="2"
|
||||||
|
label="new password"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="newPass"
|
||||||
|
>
|
||||||
|
<b-form-input id="newPass" v-model="newPass" type="password" />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="2"
|
||||||
|
label="confirm password"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="confirmPass"
|
||||||
|
>
|
||||||
|
<b-form-input id="confirmPass" v-model="confirmPass" type="password" />
|
||||||
|
</b-form-group>
|
||||||
|
</b-form-group>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="1" style="min-width: 85px">
|
||||||
|
<b-button type="submit" variant="primary">
|
||||||
|
Save
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-alert v-model="showAlert" :variant="alertVariant" dismissible>
|
||||||
|
{{ alertMsg }}
|
||||||
|
</b-alert>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-form>
|
||||||
|
</b-container>
|
||||||
|
</b-card-text>
|
||||||
|
</b-tab>
|
||||||
|
</b-tabs>
|
||||||
|
</b-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Menu from '@/components/Menu.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Configure',
|
||||||
|
middleware: 'auth',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Menu
|
||||||
|
},
|
||||||
|
|
||||||
|
async asyncData ({ app, store }) {
|
||||||
|
if (store.state.auth.isLogin) {
|
||||||
|
await store.dispatch('config/getGuiConfig')
|
||||||
|
await store.dispatch('config/getPlayoutConfig')
|
||||||
|
await store.dispatch('config/getUserConfig')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
configGui: store.state.config.configGui,
|
||||||
|
netChoices: store.state.config.netChoices,
|
||||||
|
configPlayout: store.state.config.configPlayout,
|
||||||
|
configUser: store.state.config.configUser,
|
||||||
|
oldPass: null,
|
||||||
|
newPass: null,
|
||||||
|
confirmPass: null,
|
||||||
|
showAlert: false,
|
||||||
|
alertVariant: 'success',
|
||||||
|
alertMsg: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async onSubmitGui (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
const update = await this.$store.dispatch('config/setGuiConfig', this.configGui)
|
||||||
|
|
||||||
|
if (update.status === 200) {
|
||||||
|
this.alertVariant = 'success'
|
||||||
|
this.alertMsg = 'Update GUI config success!'
|
||||||
|
} else {
|
||||||
|
this.alertVariant = 'danger'
|
||||||
|
this.alertMsg = 'Update GUI config failed!'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showAlert = true
|
||||||
|
},
|
||||||
|
async onSubmitPlayout (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
const update = await this.$store.dispatch('config/setPlayoutConfig', this.configPlayout)
|
||||||
|
|
||||||
|
if (update.status === 200) {
|
||||||
|
this.alertVariant = 'success'
|
||||||
|
this.alertMsg = 'Update playout config success!'
|
||||||
|
} else {
|
||||||
|
this.alertVariant = 'danger'
|
||||||
|
this.alertMsg = 'Update playout config failed!'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showAlert = true
|
||||||
|
},
|
||||||
|
async onSubmitUser (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
if (this.oldPass && this.newPass && this.newPass === this.confirmPass) {
|
||||||
|
this.configUser.old_password = this.oldPass
|
||||||
|
this.configUser.new_password = this.newPass
|
||||||
|
}
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
const update = await this.$store.dispatch('config/setUserConfig', this.configUser)
|
||||||
|
|
||||||
|
if (update.status === 200) {
|
||||||
|
this.alertVariant = 'success'
|
||||||
|
this.alertMsg = 'Update user profil success!'
|
||||||
|
} else {
|
||||||
|
this.alertVariant = 'danger'
|
||||||
|
this.alertMsg = 'Update user profil failed!'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showAlert = true
|
||||||
|
|
||||||
|
this.oldPass = null
|
||||||
|
this.newPass = null
|
||||||
|
this.confirmPass = null
|
||||||
|
},
|
||||||
|
|
||||||
|
resetAlert () {
|
||||||
|
this.showAlert = false
|
||||||
|
this.alertVariant = 'success'
|
||||||
|
this.alertMsg = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.config-container {
|
||||||
|
margin: 2em auto 2em auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-group {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area {
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
}
|
||||||
|
</style>
|
302
ffplayout/frontend/pages/index.vue
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="!$store.state.auth.isLogin">
|
||||||
|
<div class="logout-div" />
|
||||||
|
<b-container class="login-container">
|
||||||
|
<div>
|
||||||
|
<div class="header">
|
||||||
|
<h1>ffplayout</h1>
|
||||||
|
</div>
|
||||||
|
<b-form class="login-form" @submit.prevent="login">
|
||||||
|
<p v-if="formError" class="error">
|
||||||
|
{{ formError }}
|
||||||
|
</p>
|
||||||
|
<b-form-group id="input-group-1" label="User:" label-for="input-user">
|
||||||
|
<b-form-input id="input-user" v-model="formUsername" type="text" required placeholder="Username" />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group id="input-group-1" label="Password:" label-for="input-pass">
|
||||||
|
<b-form-input id="input-pass" v-model="formPassword" type="password" required placeholder="Password" />
|
||||||
|
</b-form-group>
|
||||||
|
<b-button type="submit" variant="primary">
|
||||||
|
Login
|
||||||
|
</b-button>
|
||||||
|
</b-form>
|
||||||
|
</div>
|
||||||
|
</b-container>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<b-container class="login-container">
|
||||||
|
<div>
|
||||||
|
<b-row cols="3">
|
||||||
|
<b-col cols="4" class="chart-col chart1">
|
||||||
|
<br>
|
||||||
|
<div class="stat-div">
|
||||||
|
<div class="stat-center" style="text-align: left;">
|
||||||
|
<h1>ffplayout</h1>
|
||||||
|
<h3 v-if="stat.system">
|
||||||
|
{{ stat.system }}<br>
|
||||||
|
{{ stat.node }}<br>
|
||||||
|
{{ stat.machine }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="4" class="chart-col chart2">
|
||||||
|
<div v-if="stat.cpu_usage || stat.cpu_usage >= 0">
|
||||||
|
<div>
|
||||||
|
<strong>CPU</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-div">
|
||||||
|
<div class="stat-center">
|
||||||
|
<div style="text-align: left;">
|
||||||
|
<strong>Usage: </strong>{{ stat.cpu_usage }}%<br>
|
||||||
|
<strong>Load: </strong> {{ stat.cpu_load[0] }} {{ stat.cpu_load[1] }} {{ stat.cpu_load[2] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="4" class="chart-col chart3">
|
||||||
|
<div v-if="stat.ram_total">
|
||||||
|
<div>
|
||||||
|
<strong>RAM</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-div">
|
||||||
|
<div class="stat-center">
|
||||||
|
<div style="text-align: left;">
|
||||||
|
<strong>Total: </strong> {{ stat.ram_total[1] }}<br>
|
||||||
|
<strong>Used: </strong> {{ stat.ram_used[1] }}<br>
|
||||||
|
<strong>Free: </strong> {{ stat.ram_free[1] }}<br>
|
||||||
|
<strong>Cached: </strong> {{ stat.ram_cached[1] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="4" class="chart-col chart4">
|
||||||
|
<div v-if="stat.swap_total">
|
||||||
|
<div>
|
||||||
|
<strong>SWAP</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-div">
|
||||||
|
<div class="stat-center">
|
||||||
|
<div style="text-align: left;">
|
||||||
|
<strong>Total: </strong> {{ stat.swap_total[1] }}<br>
|
||||||
|
<strong>Used: </strong> {{ stat.swap_used[1] }}<br>
|
||||||
|
<strong>Free: </strong> {{ stat.swap_free[1] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="4" class="chart-col chart5">
|
||||||
|
<div v-if="stat.disk_total">
|
||||||
|
<div>
|
||||||
|
<strong>DISK</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-div">
|
||||||
|
<div class="stat-center">
|
||||||
|
<div style="text-align: left;">
|
||||||
|
<strong>Total: </strong> {{ stat.disk_total[1] }}<br>
|
||||||
|
<strong>Used: </strong> {{ stat.disk_used[1] }}<br>
|
||||||
|
<strong>Free: </strong> {{ stat.disk_free[1] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="4" class="chart-col chart6">
|
||||||
|
<div v-if="stat.net_send">
|
||||||
|
<div>
|
||||||
|
<strong>NET</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-div">
|
||||||
|
<div class="stat-center">
|
||||||
|
<div style="text-align: left;">
|
||||||
|
<strong>Download: </strong> {{ stat.net_speed_recv[1] }}/s<br>
|
||||||
|
<strong>Upload: </strong> {{ stat.net_speed_send[1] }}/s<br>
|
||||||
|
<strong>Downloaded: </strong> {{ stat.net_recv[1] }}<br>
|
||||||
|
<strong>Uploaded: </strong> {{ stat.net_send[1] }}<br>
|
||||||
|
<strong>Recived Errors: </strong> {{ stat.net_errin }}<br>
|
||||||
|
<strong>Sended Errors: </strong> {{ stat.net_errout }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<b-button-group class="actions-grp">
|
||||||
|
<b-button to="/player" variant="primary">
|
||||||
|
Player
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/media" variant="primary">
|
||||||
|
Media
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/message" variant="primary">
|
||||||
|
Message
|
||||||
|
</b-button>
|
||||||
|
<b-button to="logging" variant="primary">
|
||||||
|
Logging
|
||||||
|
</b-button>
|
||||||
|
<b-button to="/configure" variant="primary">
|
||||||
|
Configure
|
||||||
|
</b-button>
|
||||||
|
<b-button variant="primary" @click="logout()">
|
||||||
|
Logout
|
||||||
|
</b-button>
|
||||||
|
</b-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
components: {},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formError: null,
|
||||||
|
formUsername: '',
|
||||||
|
formPassword: '',
|
||||||
|
interval: null,
|
||||||
|
stat: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async init () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
this.checkLogin()
|
||||||
|
},
|
||||||
|
async login () {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('auth/obtainToken', {
|
||||||
|
username: this.formUsername,
|
||||||
|
password: this.formPassword
|
||||||
|
})
|
||||||
|
this.formUsername = ''
|
||||||
|
this.formPassword = ''
|
||||||
|
this.formError = null
|
||||||
|
|
||||||
|
this.checkLogin()
|
||||||
|
} catch (e) {
|
||||||
|
this.formError = e.message
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async logout () {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.commit('auth/REMOVE_TOKEN')
|
||||||
|
await this.$store.commit('auth/UPDATE_IS_LOGIN', false)
|
||||||
|
} catch (e) {
|
||||||
|
this.formError = e.message
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkLogin () {
|
||||||
|
if (this.$store.state.auth.isLogin) {
|
||||||
|
this.sysStats()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sysStats () {
|
||||||
|
const response = await this.$axios.get('api/player/stats/?stats=all')
|
||||||
|
this.stat = response.data
|
||||||
|
|
||||||
|
if (process.browser) {
|
||||||
|
this.interval = setInterval(async () => {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
const response = await this.$axios.get('api/player/stats/?stats=all')
|
||||||
|
this.stat = response.data
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-btn {
|
||||||
|
margin: 0 auto 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-col {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 10em;
|
||||||
|
min-height: 15em;
|
||||||
|
border: solid #c3c3c3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-div {
|
||||||
|
padding-top: .5em;
|
||||||
|
position: relative;
|
||||||
|
height: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-center {
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 50%;
|
||||||
|
-ms-transform: translateY(-50%);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart1 {
|
||||||
|
background: rgba(210, 85, 23, 0.1);
|
||||||
|
}
|
||||||
|
.chart2 {
|
||||||
|
background: rgba(122, 210, 23, 0.1);
|
||||||
|
}
|
||||||
|
.chart3 {
|
||||||
|
background: rgba(23, 210, 149, 0.1);
|
||||||
|
}
|
||||||
|
.chart4 {
|
||||||
|
background: rgba(23, 160, 210, 0.1);
|
||||||
|
}
|
||||||
|
.chart5 {
|
||||||
|
background: rgba(122, 23, 210, 0.1);
|
||||||
|
}
|
||||||
|
.chart6 {
|
||||||
|
background: rgba(210, 23, 74, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.actions-grp {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 2em 0 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
146
ffplayout/frontend/pages/logging.vue
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Menu />
|
||||||
|
<b-card no-body>
|
||||||
|
<b-tabs pills card vertical>
|
||||||
|
<b-tab title="Playout" active @click="getLog('ffplayout')">
|
||||||
|
<b-container class="log-container">
|
||||||
|
<!-- eslint-disable-next-line -->
|
||||||
|
<pre v-if="currentLog" :inner-html.prop="currentLog | formatStr" class="log-content" />
|
||||||
|
</b-container>
|
||||||
|
</b-tab>
|
||||||
|
<b-tab title="Decoder" @click="getLog('decoder')">
|
||||||
|
<b-container class="log-container">
|
||||||
|
<!-- eslint-disable-next-line -->
|
||||||
|
<pre v-if="currentLog" :inner-html.prop="currentLog | formatStr" class="log-content" />
|
||||||
|
</b-container>
|
||||||
|
</b-tab>
|
||||||
|
<b-tab title="Encoder" @click="getLog('encoder')">
|
||||||
|
<b-container class="log-container">
|
||||||
|
<!-- eslint-disable-next-line -->
|
||||||
|
<pre v-if="currentLog" :inner-html.prop="currentLog | formatStr" class="log-content" />
|
||||||
|
</b-container>
|
||||||
|
</b-tab>
|
||||||
|
<b-tab title="System" @click="getSystemLog()">
|
||||||
|
<b-container class="log-container">
|
||||||
|
<!-- eslint-disable-next-line -->
|
||||||
|
<pre v-if="currentLog" :inner-html.prop="currentLog | formatStr" class="log-content" />
|
||||||
|
</b-container>
|
||||||
|
</b-tab>
|
||||||
|
</b-tabs>
|
||||||
|
</b-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// import { mapState } from 'vuex'
|
||||||
|
import Menu from '@/components/Menu.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Logging',
|
||||||
|
middleware: 'auth',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Menu
|
||||||
|
},
|
||||||
|
|
||||||
|
filters: {
|
||||||
|
formatStr (text) {
|
||||||
|
return text
|
||||||
|
.replace(/("\[.*")/g, '<span class="log-cmd">$1</span>')
|
||||||
|
.replace(/("\/.*")/g, '<span class="log-path">$1</span>')
|
||||||
|
.replace(/(\/[\w\d.\-/]+\n)/g, '<span class="log-path">$1</span>')
|
||||||
|
.replace(/((tcp|https?):\/\/[\w\d.:]+)/g, '<span class="log-url">$1</span>')
|
||||||
|
.replace(/(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[0-9,.]+\])/g, '<span class="log-time">$1</span>')
|
||||||
|
.replace(/\[INFO\]/g, '<span class="log-info">[INFO]</span>')
|
||||||
|
.replace(/\[WARNING\]/g, '<span class="log-warning">[WARNING]</span>')
|
||||||
|
.replace(/\[ERROR\]/g, '<span class="log-error">[ERROR]</span>')
|
||||||
|
.replace(/\[DEBUG\]/g, '<span class="log-debug">[DEBUG]</span>')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
currentLog: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
},
|
||||||
|
|
||||||
|
async created () {
|
||||||
|
await this.getLog('ffplayout')
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async getLog (type) {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
const response = await this.$axios.get(`api/player/log/?type=${type}`)
|
||||||
|
|
||||||
|
if (response.data.log) {
|
||||||
|
this.currentLog = response.data.log
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getSystemLog () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
const response = await this.$axios.post('api/player/system/', { run: 'log' })
|
||||||
|
|
||||||
|
if (response.data.data) {
|
||||||
|
this.currentLog = response.data.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.col-auto {
|
||||||
|
width: 122px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
max-width: calc(100% - 122px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container {
|
||||||
|
background: #1d2024;
|
||||||
|
max-width: 95%;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
color: #ececec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: #a7a7a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-info {
|
||||||
|
color: #51d1de;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-warning {
|
||||||
|
color: #e4a428;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-error {
|
||||||
|
color: #e42e28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-debug {
|
||||||
|
color: #23e493;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-path {
|
||||||
|
color: #e366cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-url {
|
||||||
|
color: #e3d666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-cmd {
|
||||||
|
color: #f1aa77;
|
||||||
|
}
|
||||||
|
</style>
|
571
ffplayout/frontend/pages/media.vue
Normal file
@ -0,0 +1,571 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Menu />
|
||||||
|
<b-container
|
||||||
|
class="browser-container"
|
||||||
|
@drop.prevent="addFile"
|
||||||
|
@dragover.prevent
|
||||||
|
@dragenter.prevent="dragEnter"
|
||||||
|
@dragleave.prevent="dragLeave"
|
||||||
|
>
|
||||||
|
<div class="drag-file" :class="fileDragClass">
|
||||||
|
<span>
|
||||||
|
<b-icon-box-arrow-in-down />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="folderTree.tree" class="browser">
|
||||||
|
<div class="bread-div">
|
||||||
|
<b-breadcrumb>
|
||||||
|
<b-breadcrumb-item
|
||||||
|
v-for="(crumb, index) in crumbs"
|
||||||
|
:key="crumb.key"
|
||||||
|
:active="index === crumbs.length - 1"
|
||||||
|
@click="getPath(extensions, crumb.path)"
|
||||||
|
>
|
||||||
|
{{ crumb.text }}
|
||||||
|
</b-breadcrumb-item>
|
||||||
|
</b-breadcrumb>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<splitpanes class="browser-row default-theme pane-row">
|
||||||
|
<pane min-size="20" size="24">
|
||||||
|
<div class="browser-div">
|
||||||
|
<perfect-scrollbar>
|
||||||
|
<b-list-group class="folder-list">
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="folder in folderTree.tree[1]"
|
||||||
|
:key="folder.key"
|
||||||
|
class="browser-item folder"
|
||||||
|
>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="1" class="browser-icons-col">
|
||||||
|
<b-icon-folder-fill class="browser-icons" />
|
||||||
|
</b-col>
|
||||||
|
<b-col class="browser-item-text">
|
||||||
|
<b-link @click="getPath(extensions, `/${folderTree.tree[0]}/${folder}`)">
|
||||||
|
{{ folder }}
|
||||||
|
</b-link>
|
||||||
|
</b-col>
|
||||||
|
<b-col v-if="folder !== '..'" cols="1" class="folder-delete">
|
||||||
|
<b-link @click="showDeleteModal('Folder', `/${folderTree.tree[0]}/${folder}`)">
|
||||||
|
<b-icon-x-circle-fill />
|
||||||
|
</b-link>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
</perfect-scrollbar>
|
||||||
|
</div>
|
||||||
|
</pane>
|
||||||
|
<pane class="files-col">
|
||||||
|
<loading
|
||||||
|
:active.sync="isLoading"
|
||||||
|
:can-cancel="false"
|
||||||
|
:is-full-page="false"
|
||||||
|
background-color="#485159"
|
||||||
|
color="#ff9c36"
|
||||||
|
/>
|
||||||
|
<div class="browser-div">
|
||||||
|
<perfect-scrollbar>
|
||||||
|
<b-list-group class="files-list">
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="file in folderTree.tree[2]"
|
||||||
|
:key="file.key"
|
||||||
|
class="browser-item"
|
||||||
|
>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="1" class="browser-icons-col">
|
||||||
|
<b-icon-film class="browser-icons" />
|
||||||
|
</b-col>
|
||||||
|
<b-col class="browser-item-text">
|
||||||
|
{{ file.file }}
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="browser-play-col">
|
||||||
|
<b-link @click="showPreviewModal(`/${folderTree.tree[0]}/${file.file}`)">
|
||||||
|
<b-icon-play-fill />
|
||||||
|
</b-link>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="browser-dur-col">
|
||||||
|
<span class="duration">{{ file.duration | toMin }}</span>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="text-center">
|
||||||
|
<b-link @click="showDeleteModal('File', `/${folderTree.tree[0]}/${file.file}`)">
|
||||||
|
<b-icon-x-circle-fill />
|
||||||
|
</b-link>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
</perfect-scrollbar>
|
||||||
|
</div>
|
||||||
|
</pane>
|
||||||
|
</splitpanes>
|
||||||
|
<b-button-group class="media-button">
|
||||||
|
<b-button title="Create Folder" variant="primary" @click="showCreateFolderModal()">
|
||||||
|
<b-icon-folder-plus />
|
||||||
|
</b-button>
|
||||||
|
<b-button title="Upload File" variant="primary" @click="showUploadModal()">
|
||||||
|
<b-icon-upload />
|
||||||
|
</b-button>
|
||||||
|
</b-button-group>
|
||||||
|
</div>
|
||||||
|
</b-container>
|
||||||
|
<b-modal
|
||||||
|
id="preview-modal"
|
||||||
|
ref="prev-modal"
|
||||||
|
size="xl"
|
||||||
|
centered
|
||||||
|
:title="`Preview: ${previewName}`"
|
||||||
|
hide-footer
|
||||||
|
>
|
||||||
|
<b-img v-if="isImage" :src="previewSource" fluid :alt="previewName" />
|
||||||
|
<video-player v-else-if="!isImage && previewOptions" reference="previewPlayer" :options="previewOptions" />
|
||||||
|
</b-modal>
|
||||||
|
|
||||||
|
<b-modal
|
||||||
|
id="folder-modal"
|
||||||
|
ref="folder-modal"
|
||||||
|
size="xl"
|
||||||
|
centered
|
||||||
|
title="Create Folder"
|
||||||
|
hide-footer
|
||||||
|
>
|
||||||
|
<b-form @submit="onSubmitCreateFolder" @reset="onCancelCreateFolder">
|
||||||
|
<b-form-input
|
||||||
|
id="folder-name"
|
||||||
|
v-model="folderName"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Enter a unique folder name"
|
||||||
|
/>
|
||||||
|
<div class="media-button">
|
||||||
|
<b-button type="submit" variant="primary">
|
||||||
|
Create
|
||||||
|
</b-button>
|
||||||
|
<b-button type="reset" variant="primary">
|
||||||
|
Cancel
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-form>
|
||||||
|
</b-modal>
|
||||||
|
|
||||||
|
<b-modal
|
||||||
|
id="upload-modal"
|
||||||
|
ref="up-modal"
|
||||||
|
size="xl"
|
||||||
|
centered
|
||||||
|
title="File Upload"
|
||||||
|
hide-footer
|
||||||
|
>
|
||||||
|
<b-form @submit="onSubmitUpload" @reset="onResetUpload">
|
||||||
|
<b-form-file
|
||||||
|
v-model="inputFiles"
|
||||||
|
:state="Boolean(inputFiles)"
|
||||||
|
:placeholder="inputPlaceholder"
|
||||||
|
drop-placeholder="Drop files here..."
|
||||||
|
multiple
|
||||||
|
:accept="extensions.replace(/ /g, ', ')"
|
||||||
|
:file-name-formatter="formatNames"
|
||||||
|
/>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="10">
|
||||||
|
<b-row class="progress-row">
|
||||||
|
<b-col cols="1">
|
||||||
|
Overall:
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="10">
|
||||||
|
<b-progress :value="overallProgress" />
|
||||||
|
</b-col>
|
||||||
|
<div class="w-100" />
|
||||||
|
<b-col cols="1">
|
||||||
|
Current:
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="10">
|
||||||
|
<b-progress :value="currentProgress" />
|
||||||
|
</b-col>
|
||||||
|
<div class="w-100" />
|
||||||
|
<b-col cols="1">
|
||||||
|
Uploading:
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="10">
|
||||||
|
<strong>{{ uploadTask }}</strong>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="2">
|
||||||
|
<div class="media-button">
|
||||||
|
<b-button type="submit" variant="primary">
|
||||||
|
Upload
|
||||||
|
</b-button>
|
||||||
|
<b-button type="reset" variant="primary">
|
||||||
|
Cancel
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-form>
|
||||||
|
</b-modal>
|
||||||
|
<b-modal id="delete-modal" :title="`Delete ${deleteType}`" centered hide-footer>
|
||||||
|
<p>
|
||||||
|
Are you sure that you want to delete:<br>
|
||||||
|
<strong>{{ previewName }}</strong>
|
||||||
|
</p>
|
||||||
|
<div class="media-button">
|
||||||
|
<b-button variant="primary" @click="deleteFileOrFolder()">
|
||||||
|
Ok
|
||||||
|
</b-button>
|
||||||
|
<b-button variant="primary" @click="cancelDelete()">
|
||||||
|
Cancel
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import Menu from '@/components/Menu.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Media',
|
||||||
|
middleware: 'auth',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Menu
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
fileDragClass: '',
|
||||||
|
extensions: '',
|
||||||
|
folderName: '',
|
||||||
|
inputFiles: [],
|
||||||
|
inputPlaceholder: 'Choose files or drop them here...',
|
||||||
|
previewOptions: {},
|
||||||
|
previewComp: null,
|
||||||
|
previewName: '',
|
||||||
|
previewSource: '',
|
||||||
|
deleteType: 'File',
|
||||||
|
deleteSource: '',
|
||||||
|
isImage: false,
|
||||||
|
uploadTask: '',
|
||||||
|
overallProgress: 0,
|
||||||
|
currentProgress: 0,
|
||||||
|
cancelTokenSource: this.$axios.CancelToken.source(),
|
||||||
|
lastPath: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState('config', ['configGui', 'configPlayout']),
|
||||||
|
...mapState('media', ['crumbs', 'folderTree'])
|
||||||
|
},
|
||||||
|
|
||||||
|
async created () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
await this.$store.dispatch('config/getGuiConfig')
|
||||||
|
await this.$store.dispatch('config/getPlayoutConfig')
|
||||||
|
|
||||||
|
this.extensions = [...this.configPlayout.storage.extensions, ...this.configGui.extra_extensions].join(' ')
|
||||||
|
this.getPath(this.extensions, '')
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async getPath (extensions, path) {
|
||||||
|
this.lastPath = path
|
||||||
|
this.isLoading = true
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
await this.$store.dispatch('media/getTree', { extensions, path })
|
||||||
|
this.isLoading = false
|
||||||
|
},
|
||||||
|
|
||||||
|
dragEnter (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
this.fileDragClass = 'drop-file-visible'
|
||||||
|
},
|
||||||
|
|
||||||
|
dragLeave (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
this.fileDragClass = ''
|
||||||
|
this.inputPlaceholder = 'Choose files or drop them here...'
|
||||||
|
},
|
||||||
|
|
||||||
|
addFile (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
const droppedFiles = evt.dataTransfer.files
|
||||||
|
if (!droppedFiles) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
([...droppedFiles]).forEach((f) => {
|
||||||
|
this.inputFiles.push(f)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.inputFiles.length === 1) {
|
||||||
|
this.inputPlaceholder = this.inputFiles[0].name
|
||||||
|
} else {
|
||||||
|
this.inputPlaceholder = `${this.inputFiles.length} files selected`
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fileDragClass = ''
|
||||||
|
this.showUploadModal()
|
||||||
|
},
|
||||||
|
|
||||||
|
showCreateFolderModal () {
|
||||||
|
this.$root.$emit('bv::show::modal', 'folder-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
async onSubmitCreateFolder (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
|
||||||
|
await this.$axios.post(
|
||||||
|
'api/player/media/op/',
|
||||||
|
{ folder: this.folderName, path: this.crumbs.map(e => e.text).join('/') }
|
||||||
|
)
|
||||||
|
|
||||||
|
this.$root.$emit('bv::hide::modal', 'folder-modal')
|
||||||
|
this.getPath(this.extensions, this.lastPath)
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancelCreateFolder (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
this.$root.$emit('bv::hide::modal', 'folder-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
showUploadModal () {
|
||||||
|
this.uploadTask = ''
|
||||||
|
this.currentProgress = 0
|
||||||
|
this.overallProgress = 0
|
||||||
|
this.$root.$emit('bv::show::modal', 'upload-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
formatNames (files) {
|
||||||
|
if (files.length === 1) {
|
||||||
|
return files[0].name
|
||||||
|
} else {
|
||||||
|
return `${files.length} files selected`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onSubmitUpload (evt) {
|
||||||
|
// console.log(evt)
|
||||||
|
evt.preventDefault()
|
||||||
|
const uploadProgress = fileName => (progressEvent) => {
|
||||||
|
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||||
|
this.currentProgress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [i, file] of this.inputFiles.entries()) {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
this.uploadTask = file.name
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
onUploadProgress: uploadProgress(file.name),
|
||||||
|
cancelToken: this.cancelTokenSource.token,
|
||||||
|
headers: { Authorization: 'Bearer ' + this.$store.state.auth.jwtToken }
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$axios.put(
|
||||||
|
`api/player/media/upload/${encodeURIComponent(file.name)}?path=${encodeURIComponent(this.crumbs.map(e => e.text).join('/'))}`,
|
||||||
|
file,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
this.overallProgress = (i + 1) * 100 / this.inputFiles.length,
|
||||||
|
this.currentProgress = 0
|
||||||
|
)
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uploadTask = 'Done...'
|
||||||
|
this.inputPlaceholder = 'Choose files or drop them here...'
|
||||||
|
this.inputFiles = []
|
||||||
|
this.getPath(this.extensions, this.lastPath)
|
||||||
|
this.$root.$emit('bv::hide::modal', 'upload-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
onResetUpload (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
this.inputFiles = []
|
||||||
|
this.overallProgress = 0
|
||||||
|
this.currentProgress = 0
|
||||||
|
this.uploadTask = ''
|
||||||
|
this.inputPlaceholder = 'Choose files or drop them here...'
|
||||||
|
|
||||||
|
this.cancelTokenSource.cancel('Upload cancelled')
|
||||||
|
this.getPath(this.extensions, this.lastPath)
|
||||||
|
|
||||||
|
this.$root.$emit('bv::hide::modal', 'upload-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
showPreviewModal (src) {
|
||||||
|
this.previewSource = src
|
||||||
|
this.previewName = src.split('/').slice(-1)[0]
|
||||||
|
const ext = this.previewName.split('.').slice(-1)[0]
|
||||||
|
|
||||||
|
if (this.configPlayout.storage.extensions.includes(`.${ext}`)) {
|
||||||
|
this.isImage = false
|
||||||
|
this.previewOptions = {
|
||||||
|
liveui: false,
|
||||||
|
controls: true,
|
||||||
|
suppressNotSupportedError: true,
|
||||||
|
autoplay: false,
|
||||||
|
preload: 'auto',
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
type: `video/${ext}`,
|
||||||
|
src: encodeURIComponent(src)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isImage = true
|
||||||
|
}
|
||||||
|
this.$root.$emit('bv::show::modal', 'preview-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
showDeleteModal (type, src) {
|
||||||
|
this.deleteSource = src
|
||||||
|
|
||||||
|
if (type === 'File') {
|
||||||
|
this.previewName = src.split('/').slice(-1)[0]
|
||||||
|
} else {
|
||||||
|
this.previewName = src
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deleteType = type
|
||||||
|
this.$root.$emit('bv::show::modal', 'delete-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteFileOrFolder () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
let file
|
||||||
|
let pathName
|
||||||
|
|
||||||
|
if (this.deleteType === 'File') {
|
||||||
|
file = this.deleteSource.split('/').slice(-1)[0]
|
||||||
|
pathName = this.deleteSource.substring(0, this.deleteSource.lastIndexOf('/') + 1)
|
||||||
|
} else {
|
||||||
|
file = null
|
||||||
|
pathName = this.deleteSource
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$axios.delete(`api/player/media/op/?file=${encodeURIComponent(file)}&path=${encodeURIComponent(pathName)}`)
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
|
||||||
|
this.$root.$emit('bv::hide::modal', 'delete-modal')
|
||||||
|
|
||||||
|
this.getPath(this.extensions, this.lastPath)
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelDelete () {
|
||||||
|
this.deleteSource = ''
|
||||||
|
this.$root.$emit('bv::hide::modal', 'delete-modal')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.browser-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-file {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
background: rgba(48, 54, 61, 0.75);
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
height: calc(100% - 80px);
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: .25em;
|
||||||
|
z-index: 2;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-file-visible {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-file span {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bread-div {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-div {
|
||||||
|
background: #30363d;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-row {
|
||||||
|
height: calc(100% - 90px);
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-col {
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 460px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder:hover > div > .folder-delete {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-list {
|
||||||
|
height: 100%;
|
||||||
|
padding: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-delete {
|
||||||
|
margin-right: .5em;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-col {
|
||||||
|
min-width: 320px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-list {
|
||||||
|
width: 99.5%;
|
||||||
|
height: 100%;
|
||||||
|
padding: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-button {
|
||||||
|
float: right;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-row {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-row .col-1 {
|
||||||
|
min-width: 60px
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-row .col-10 {
|
||||||
|
margin: auto 0 auto 0
|
||||||
|
}
|
||||||
|
</style>
|
435
ffplayout/frontend/pages/message.vue
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Menu />
|
||||||
|
<b-container class="messege-container">
|
||||||
|
<div class="preset-div">
|
||||||
|
<b-row>
|
||||||
|
<b-col>
|
||||||
|
<b-form-select v-model="selected" :options="presets" />
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="2">
|
||||||
|
<b-button-group class="mr-1">
|
||||||
|
<b-button title="Save Preset" variant="primary" @click="savePreset()">
|
||||||
|
<b-icon icon="cloud-upload" />
|
||||||
|
</b-button>
|
||||||
|
<b-button title="New Preset" variant="primary" @click="openDialog()">
|
||||||
|
<b-icon-file-plus />
|
||||||
|
</b-button>
|
||||||
|
<b-button title="Delete Preset" variant="primary" @click="deleteDialog()">
|
||||||
|
<b-icon-file-minus />
|
||||||
|
</b-button>
|
||||||
|
</b-button-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</div>
|
||||||
|
<b-form @submit.prevent="submitMessage">
|
||||||
|
<b-form-group>
|
||||||
|
<b-form-textarea
|
||||||
|
v-model="form.text"
|
||||||
|
placeholder="Message"
|
||||||
|
rows="7"
|
||||||
|
class="message"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
|
<b-row>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group>
|
||||||
|
<b-form-input
|
||||||
|
id="input-1"
|
||||||
|
v-model="form.x"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="X"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
|
<b-form-group>
|
||||||
|
<b-form-input
|
||||||
|
id="input-2"
|
||||||
|
v-model="form.y"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Y"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
|
<b-row>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group
|
||||||
|
label="Size"
|
||||||
|
label-for="input-3"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="input-3"
|
||||||
|
v-model="form.fontSize"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
value="24"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group
|
||||||
|
label="Spacing"
|
||||||
|
label-for="input-4"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="input-4"
|
||||||
|
v-model="form.fontSpacing"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
value="4"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<b-row>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group
|
||||||
|
label="Font Color"
|
||||||
|
label-for="input-5"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="input-5"
|
||||||
|
v-model="form.fontColor"
|
||||||
|
type="color"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group
|
||||||
|
label="Font Alpha"
|
||||||
|
label-for="input-6"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="input-6"
|
||||||
|
v-model="form.fontAlpha"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-form-checkbox
|
||||||
|
v-model="form.showBox"
|
||||||
|
style="margin-bottom: 8px;"
|
||||||
|
>
|
||||||
|
Show Box
|
||||||
|
</b-form-checkbox>
|
||||||
|
|
||||||
|
<b-row>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group
|
||||||
|
label="Box Color"
|
||||||
|
label-for="input-7"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="input-7"
|
||||||
|
v-model="form.boxColor"
|
||||||
|
type="color"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-form-group
|
||||||
|
label="Box Alpha"
|
||||||
|
label-for="input-8"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="input-8"
|
||||||
|
v-model="form.boxAlpha"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-form-group
|
||||||
|
label="Border Width"
|
||||||
|
label-for="input-9"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="input-9"
|
||||||
|
v-model="form.border"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
value="4"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<b-form-group
|
||||||
|
label="Overall Alpha"
|
||||||
|
label-for="input-10"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="input-10"
|
||||||
|
v-model="form.overallAlpha"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
|
<b-row>
|
||||||
|
<b-col class="sub-btn">
|
||||||
|
<b-button type="submit" class="send-btn" variant="primary">
|
||||||
|
Send
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-alert variant="success" :show="success" dismissible @dismissed="success=false">
|
||||||
|
Sending success...
|
||||||
|
</b-alert>
|
||||||
|
<b-alert variant="warning" :show="failed" dismissible @dismissed="success=failed">
|
||||||
|
Sending failed...
|
||||||
|
</b-alert>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-form>
|
||||||
|
</b-container>
|
||||||
|
<b-modal
|
||||||
|
id="create-modal"
|
||||||
|
ref="create-modal"
|
||||||
|
title="Create Preset"
|
||||||
|
@ok="handleCreate"
|
||||||
|
>
|
||||||
|
<form ref="form" @submit.stop.prevent="createPreset">
|
||||||
|
<b-form-group label="Name" label-for="name-input" invalid-feedback="Name is required">
|
||||||
|
<b-form-input id="name-input" v-model="newPresetName" required />
|
||||||
|
</b-form-group>
|
||||||
|
</form>
|
||||||
|
</b-modal>
|
||||||
|
<b-modal
|
||||||
|
id="delete-modal"
|
||||||
|
ref="delete-modal"
|
||||||
|
title="Delete Preset"
|
||||||
|
@ok="handleDelete"
|
||||||
|
>
|
||||||
|
<strong>Delete: "{{ selected }}"?</strong>
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// import { mapState } from 'vuex'
|
||||||
|
import Menu from '@/components/Menu.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Media',
|
||||||
|
middleware: 'auth',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Menu
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
text: '',
|
||||||
|
x: '0',
|
||||||
|
y: '0',
|
||||||
|
fontSize: 24,
|
||||||
|
fontSpacing: 4,
|
||||||
|
fontColor: '#ffffff',
|
||||||
|
fontAlpha: 1.0,
|
||||||
|
showBox: true,
|
||||||
|
boxColor: '#000000',
|
||||||
|
boxAlpha: 0.8,
|
||||||
|
border: 4,
|
||||||
|
overallAlpha: 1
|
||||||
|
},
|
||||||
|
selected: null,
|
||||||
|
newPresetName: '',
|
||||||
|
presets: [],
|
||||||
|
success: false,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
selected (name) {
|
||||||
|
this.getPreset(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created () {
|
||||||
|
this.getPreset('')
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async getPreset (preset) {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
let req = ''
|
||||||
|
|
||||||
|
if (preset) {
|
||||||
|
req = `?name=${preset}`
|
||||||
|
}
|
||||||
|
const response = await this.$axios.get(`api/player/messenger/${req}`)
|
||||||
|
|
||||||
|
if (response.data && !preset) {
|
||||||
|
for (const item of response.data) {
|
||||||
|
this.presets.push({ value: item.name, text: item.name })
|
||||||
|
}
|
||||||
|
} else if (response.data) {
|
||||||
|
this.form = {
|
||||||
|
id: response.data[0].id,
|
||||||
|
name: response.data[0].name,
|
||||||
|
text: response.data[0].message,
|
||||||
|
x: response.data[0].x,
|
||||||
|
y: response.data[0].y,
|
||||||
|
fontSize: response.data[0].font_size,
|
||||||
|
fontSpacing: response.data[0].font_spacing,
|
||||||
|
fontColor: response.data[0].font_color,
|
||||||
|
fontAlpha: response.data[0].font_alpha,
|
||||||
|
showBox: response.data[0].show_box,
|
||||||
|
boxColor: response.data[0].box_color,
|
||||||
|
boxAlpha: response.data[0].box_alpha,
|
||||||
|
border: response.data[0].border_width,
|
||||||
|
overallAlpha: response.data[0].overall_alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openDialog () {
|
||||||
|
this.$bvModal.show('create-modal')
|
||||||
|
},
|
||||||
|
handleCreate (bvModalEvt) {
|
||||||
|
// Prevent modal from closing
|
||||||
|
bvModalEvt.preventDefault()
|
||||||
|
// Trigger submit handler
|
||||||
|
this.createPreset()
|
||||||
|
},
|
||||||
|
async createPreset () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
const preset = {
|
||||||
|
name: this.newPresetName,
|
||||||
|
message: this.form.text,
|
||||||
|
x: this.form.x,
|
||||||
|
y: this.form.y,
|
||||||
|
font_size: this.form.fontSize,
|
||||||
|
font_spacing: this.form.fontSpacing,
|
||||||
|
font_color: this.form.fontColor,
|
||||||
|
font_alpha: this.form.fontAlpha,
|
||||||
|
show_box: this.form.showBox,
|
||||||
|
box_color: this.form.boxColor,
|
||||||
|
box_alpha: this.form.boxAlpha,
|
||||||
|
border_width: this.form.border,
|
||||||
|
overall_alpha: this.form.overallAlpha
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.$axios.post('api/player/messenger/', preset)
|
||||||
|
|
||||||
|
if (response.status === 201) {
|
||||||
|
this.success = true
|
||||||
|
} else {
|
||||||
|
this.failed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$bvModal.hide('create-modal')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async savePreset () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
if (this.selected) {
|
||||||
|
const preset = {
|
||||||
|
id: this.form.id,
|
||||||
|
name: this.form.name,
|
||||||
|
message: this.form.text,
|
||||||
|
x: this.form.x,
|
||||||
|
y: this.form.y,
|
||||||
|
font_size: this.form.fontSize,
|
||||||
|
font_spacing: this.form.fontSpacing,
|
||||||
|
font_color: this.form.fontColor,
|
||||||
|
font_alpha: this.form.fontAlpha,
|
||||||
|
show_box: this.form.showBox,
|
||||||
|
box_color: this.form.boxColor,
|
||||||
|
box_alpha: this.form.boxAlpha,
|
||||||
|
border_width: this.form.border,
|
||||||
|
overall_alpha: this.form.overallAlpha
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.$axios.put(`api/player/messenger/${this.form.id}/`, preset)
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.success = true
|
||||||
|
} else {
|
||||||
|
this.failed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDialog () {
|
||||||
|
this.$bvModal.show('delete-modal')
|
||||||
|
},
|
||||||
|
handleDelete (evt) {
|
||||||
|
evt.preventDefault()
|
||||||
|
this.deletePreset()
|
||||||
|
},
|
||||||
|
async deletePreset () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
if (this.selected) {
|
||||||
|
await this.$axios.delete(`api/player/messenger/${this.form.id}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$bvModal.hide('delete-modal')
|
||||||
|
this.getPreset('')
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitMessage () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
function aToHex (num) {
|
||||||
|
return '0x' + Math.round(num * 255).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
text: this.form.text,
|
||||||
|
x: this.form.x,
|
||||||
|
y: this.form.y,
|
||||||
|
fontsize: this.form.fontSize,
|
||||||
|
line_spacing: this.form.fontSpacing,
|
||||||
|
fontcolor: this.form.fontColor + '@' + aToHex(this.form.fontAlpha),
|
||||||
|
alpha: this.form.overallAlpha,
|
||||||
|
box: (this.form.showBox) ? 1 : 0,
|
||||||
|
boxcolor: this.form.boxColor + '@' + aToHex(this.form.boxAlpha),
|
||||||
|
boxborderw: this.form.border
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.$axios.post('api/player/messenger/send/', { data: obj })
|
||||||
|
|
||||||
|
if (response.data && response.data.status.Success && response.data.status.Success === '0 Success') {
|
||||||
|
this.success = true
|
||||||
|
} else {
|
||||||
|
this.failed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.messege-container {
|
||||||
|
margin-top: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-div {
|
||||||
|
width: 50%;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
673
ffplayout/frontend/pages/player.vue
Normal file
@ -0,0 +1,673 @@
|
|||||||
|
<template>
|
||||||
|
<div style="height:100%;">
|
||||||
|
<Menu />
|
||||||
|
<b-container class="control-container">
|
||||||
|
<b-row class="control-row">
|
||||||
|
<b-col cols="3" class="player-col">
|
||||||
|
<b-aspect aspect="16:9">
|
||||||
|
<video-player v-if="videoOptions.sources" reference="videoPlayer" :options="videoOptions" />
|
||||||
|
</b-aspect>
|
||||||
|
</b-col>
|
||||||
|
<b-col class="control-col">
|
||||||
|
<b-row class="control-col">
|
||||||
|
<b-col cols="8" class="status-col">
|
||||||
|
<b-row class="status-row">
|
||||||
|
<b-col class="time-col clock-col">
|
||||||
|
<div class="time-str">
|
||||||
|
{{ timeStr }}
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col class="time-col counter-col">
|
||||||
|
<div class="time-str">
|
||||||
|
{{ timeLeft }}
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<div class="w-100" />
|
||||||
|
<b-col class="current-clip" align-self="end">
|
||||||
|
<div class="current-clip-text">
|
||||||
|
{{ currentClip | filename }}
|
||||||
|
</div>
|
||||||
|
<div class="current-clip-progress">
|
||||||
|
<b-progress :value="progressValue" variant="warning" />
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="4" class="control-unit-col">
|
||||||
|
<b-row class="control-unit-row">
|
||||||
|
<b-col>
|
||||||
|
<div>
|
||||||
|
<b-button
|
||||||
|
title="Start Playout Service"
|
||||||
|
class="control-button control-button-play"
|
||||||
|
:class="isPlaying"
|
||||||
|
variant="primary"
|
||||||
|
@click="playoutControl('start')"
|
||||||
|
>
|
||||||
|
<b-icon-play />
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<div>
|
||||||
|
<b-button
|
||||||
|
title="Stop Playout Service"
|
||||||
|
class="control-button control-button-stop"
|
||||||
|
variant="primary"
|
||||||
|
@click="playoutControl('stop')"
|
||||||
|
>
|
||||||
|
<b-icon-stop />
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<div class="w-100" />
|
||||||
|
<b-col>
|
||||||
|
<div>
|
||||||
|
<b-button
|
||||||
|
title="Reload Playout Service"
|
||||||
|
class="control-button control-button-reload"
|
||||||
|
variant="primary"
|
||||||
|
@click="playoutControl('reload')"
|
||||||
|
>
|
||||||
|
<b-icon-arrow-repeat />
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<div>
|
||||||
|
<b-button
|
||||||
|
title="Restart Playout Service"
|
||||||
|
class="control-button control-button-restart"
|
||||||
|
variant="primary"
|
||||||
|
@click="playoutControl('restart')"
|
||||||
|
>
|
||||||
|
<b-icon-arrow-clockwise />
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-row class="date-row">
|
||||||
|
<b-col>
|
||||||
|
<b-datepicker v-model="listDate" size="sm" class="date-div" offset="-35px" />
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<splitpanes class="list-row default-theme pane-row">
|
||||||
|
<pane min-size="20" size="24">
|
||||||
|
<loading
|
||||||
|
:active.sync="isLoading"
|
||||||
|
:can-cancel="false"
|
||||||
|
:is-full-page="false"
|
||||||
|
background-color="#485159"
|
||||||
|
color="#ff9c36"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="folderTree.tree" class="browser-div">
|
||||||
|
<div>
|
||||||
|
<b-breadcrumb>
|
||||||
|
<b-breadcrumb-item
|
||||||
|
v-for="(crumb, index) in crumbs"
|
||||||
|
:key="crumb.key"
|
||||||
|
:active="index === crumbs.length - 1"
|
||||||
|
@click="getPath(extensions, crumb.path)"
|
||||||
|
>
|
||||||
|
{{ crumb.text }}
|
||||||
|
</b-breadcrumb-item>
|
||||||
|
</b-breadcrumb>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<perfect-scrollbar>
|
||||||
|
<b-list-group>
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="folder in folderTree.tree[1]"
|
||||||
|
:key="folder.key"
|
||||||
|
class="browser-item"
|
||||||
|
>
|
||||||
|
<b-link @click="getPath(extensions, `/${folderTree.tree[0]}/${folder}`)">
|
||||||
|
<b-icon-folder-fill class="browser-icons" /> {{ folder }}
|
||||||
|
</b-link>
|
||||||
|
</b-list-group-item>
|
||||||
|
<draggable
|
||||||
|
:list="folderTree.tree[2]"
|
||||||
|
:clone="cloneClip"
|
||||||
|
:group="{ name: 'playlist', pull: 'clone', put: false }"
|
||||||
|
:sort="false"
|
||||||
|
>
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="file in folderTree.tree[2]"
|
||||||
|
:key="file.key"
|
||||||
|
class="browser-item"
|
||||||
|
>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="1" class="browser-icons-col">
|
||||||
|
<b-icon-film class="browser-icons" />
|
||||||
|
</b-col>
|
||||||
|
<b-col class="browser-item-text grabbing">
|
||||||
|
{{ file.file }}
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="browser-play-col">
|
||||||
|
<b-link @click="showModal(`/${folderTree.tree[0]}/${file.file}`)">
|
||||||
|
<b-icon-play-fill />
|
||||||
|
</b-link>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="browser-dur-col">
|
||||||
|
<span class="duration">{{ file.duration | toMin }}</span>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-list-group-item>
|
||||||
|
</draggable>
|
||||||
|
</b-list-group>
|
||||||
|
</perfect-scrollbar>
|
||||||
|
</div>
|
||||||
|
</pane>
|
||||||
|
<pane>
|
||||||
|
<div class="playlist-container">
|
||||||
|
<b-list-group>
|
||||||
|
<b-list-group-item>
|
||||||
|
<b-row class="playlist-row">
|
||||||
|
<b-col cols="1" class="timecode">
|
||||||
|
Start
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
File
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="text-center playlist-input">
|
||||||
|
Play
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="timecode">
|
||||||
|
Duration
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="timecode">
|
||||||
|
In
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="timecode">
|
||||||
|
Out
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="text-center playlist-input">
|
||||||
|
Ad
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="text-center playlist-input">
|
||||||
|
Delete
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
<perfect-scrollbar>
|
||||||
|
<b-list-group>
|
||||||
|
<draggable
|
||||||
|
v-model="playlist"
|
||||||
|
group="playlist"
|
||||||
|
@start="drag=true"
|
||||||
|
@end="drag=false"
|
||||||
|
>
|
||||||
|
<b-list-group-item v-for="(item, index) in playlist" :key="item.key" class="playlist-item">
|
||||||
|
<b-row class="playlist-row">
|
||||||
|
<b-col cols="1" class="timecode">
|
||||||
|
{{ item.begin | secondsToTime }}
|
||||||
|
</b-col>
|
||||||
|
<b-col class="grabbing">
|
||||||
|
{{ item.source | filename }}
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="text-center playlist-input">
|
||||||
|
<b-link @click="showModal(item.source)">
|
||||||
|
<b-icon-play-fill />
|
||||||
|
</b-link>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" text class="timecode">
|
||||||
|
{{ item.duration | secondsToTime }}
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="timecode">
|
||||||
|
<b-form-input :value="item.in | secondsToTime" size="sm" @input="changeTime('in', index, $event)" />
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="timecode">
|
||||||
|
<b-form-input :value="item.out | secondsToTime" size="sm" @input="changeTime('out', index, $event)" />
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="text-center playlist-input">
|
||||||
|
<b-form-checkbox
|
||||||
|
v-model="item.category"
|
||||||
|
value="advertisement"
|
||||||
|
:unchecked-value="item.category"
|
||||||
|
/>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="text-center playlist-input">
|
||||||
|
<b-link @click="removeItemFromPlaylist(index)">
|
||||||
|
<b-icon-x-circle-fill />
|
||||||
|
</b-link>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-list-group-item>
|
||||||
|
</draggable>
|
||||||
|
</b-list-group>
|
||||||
|
</perfect-scrollbar>
|
||||||
|
</div>
|
||||||
|
</pane>
|
||||||
|
</splitpanes>
|
||||||
|
<b-button-group class="media-button">
|
||||||
|
<b-button v-b-tooltip.hover title="Reset Playlist" variant="primary" @click="resetPlaylist()">
|
||||||
|
<b-icon-arrow-counterclockwise />
|
||||||
|
</b-button>
|
||||||
|
<b-button v-b-tooltip.hover title="Save Playlist" variant="primary" @click="savePlaylist()">
|
||||||
|
<b-icon-download />
|
||||||
|
</b-button>
|
||||||
|
</b-button-group>
|
||||||
|
</b-container>
|
||||||
|
<b-modal
|
||||||
|
id="preview-modal"
|
||||||
|
ref="prev-modal"
|
||||||
|
size="xl"
|
||||||
|
centered
|
||||||
|
:title="`Preview: ${previewSource}`"
|
||||||
|
hide-footer
|
||||||
|
>
|
||||||
|
<video-player v-if="previewOptions" reference="previewPlayer" :options="previewOptions" />
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import Menu from '@/components/Menu.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Player',
|
||||||
|
middleware: 'auth',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Menu
|
||||||
|
},
|
||||||
|
|
||||||
|
filters: {
|
||||||
|
secondsToTime (sec) {
|
||||||
|
return new Date(sec * 1000).toISOString().substr(11, 8)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
isPlaying: '',
|
||||||
|
listDate: this.$dayjs().format('YYYY-MM-DD'),
|
||||||
|
interval: null,
|
||||||
|
extensions: '',
|
||||||
|
videoOptions: {},
|
||||||
|
previewOptions: {},
|
||||||
|
previewComp: null,
|
||||||
|
previewSource: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState('config', ['configGui', 'configPlayout']),
|
||||||
|
...mapState('media', ['crumbs', 'folderTree']),
|
||||||
|
...mapState('playlist', ['timeStr', 'timeLeft', 'currentClip', 'progressValue']),
|
||||||
|
playlist: {
|
||||||
|
get () {
|
||||||
|
return this.$store.state.playlist.playlist
|
||||||
|
},
|
||||||
|
set (list) {
|
||||||
|
this.$store.commit('playlist/UPDATE_PLAYLIST', this.$processPlaylist(
|
||||||
|
this.configPlayout.playlist.day_start, list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
listDate (date) {
|
||||||
|
this.getPlaylist()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async created () {
|
||||||
|
await this.getConfig()
|
||||||
|
await this.getStatus()
|
||||||
|
|
||||||
|
this.extensions = this.configPlayout.storage.extensions.join(' ')
|
||||||
|
|
||||||
|
await this.getPath(this.extensions, '')
|
||||||
|
|
||||||
|
this.videoOptions = {
|
||||||
|
liveui: true,
|
||||||
|
controls: true,
|
||||||
|
suppressNotSupportedError: true,
|
||||||
|
autoplay: false,
|
||||||
|
preload: 'auto',
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
type: 'application/x-mpegURL',
|
||||||
|
src: this.configGui.player_url
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.getPlaylist()
|
||||||
|
|
||||||
|
if (!process.env.DEV) {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.$store.dispatch('playlist/animClock', { dayStart: this.configPlayout.playlist.day_start })
|
||||||
|
}, 5000)
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('playlist/animClock', { dayStart: this.configPlayout.playlist.day_start })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy () {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async getConfig () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
await this.$store.dispatch('config/getGuiConfig')
|
||||||
|
await this.$store.dispatch('config/getPlayoutConfig')
|
||||||
|
},
|
||||||
|
async getPath (extensions, path) {
|
||||||
|
this.isLoading = true
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
await this.$store.dispatch('media/getTree', { extensions, path })
|
||||||
|
this.isLoading = false
|
||||||
|
},
|
||||||
|
async getStatus () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
|
||||||
|
const status = await this.$axios.post('api/player/system/', { run: 'status' })
|
||||||
|
|
||||||
|
if (status.data.data && status.data.data === 'active') {
|
||||||
|
this.isPlaying = 'is-playing'
|
||||||
|
} else {
|
||||||
|
this.isPlaying = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async playoutControl (state) {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
await this.$axios.post('api/player/system/', { run: state })
|
||||||
|
|
||||||
|
setTimeout(() => { this.getStatus() }, 1000)
|
||||||
|
},
|
||||||
|
async getPlaylist () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
await this.$store.dispatch('playlist/getPlaylist', { dayStart: this.configPlayout.playlist.day_start, date: this.listDate })
|
||||||
|
},
|
||||||
|
showModal (src) {
|
||||||
|
this.previewSource = src.split('/').slice(-1)[0]
|
||||||
|
const ext = this.previewSource.split('.').slice(-1)[0]
|
||||||
|
this.previewOptions = {
|
||||||
|
liveui: false,
|
||||||
|
controls: true,
|
||||||
|
suppressNotSupportedError: true,
|
||||||
|
autoplay: false,
|
||||||
|
preload: 'auto',
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
type: `video/${ext}`,
|
||||||
|
src: encodeURIComponent(src)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.$root.$emit('bv::show::modal', 'preview-modal')
|
||||||
|
},
|
||||||
|
cloneClip ({ file, duration }) {
|
||||||
|
return {
|
||||||
|
source: `/${this.folderTree.tree[0]}/${file}`,
|
||||||
|
in: 0,
|
||||||
|
out: duration,
|
||||||
|
duration
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeTime (pos, index, input) {
|
||||||
|
if (input.match(/(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)/gm)) {
|
||||||
|
const sec = this.$timeToSeconds(input)
|
||||||
|
|
||||||
|
if (pos === 'in') {
|
||||||
|
this.playlist[index].in = sec
|
||||||
|
} else if (pos === 'out') {
|
||||||
|
this.playlist[index].out = sec
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.commit('playlist/UPDATE_PLAYLIST', this.$processPlaylist(
|
||||||
|
this.configPlayout.playlist.day_start, this.playlist))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeItemFromPlaylist (index) {
|
||||||
|
this.playlist.splice(index, 1)
|
||||||
|
|
||||||
|
this.$store.commit('playlist/UPDATE_PLAYLIST', this.$processPlaylist(
|
||||||
|
this.configPlayout.playlist.day_start, this.playlist))
|
||||||
|
},
|
||||||
|
async resetPlaylist () {
|
||||||
|
await this.$store.dispatch('playlist/getPlaylist', { dayStart: this.configPlayout.playlist.day_start, date: this.listDate })
|
||||||
|
},
|
||||||
|
async savePlaylist () {
|
||||||
|
await this.$store.dispatch('auth/inspectToken')
|
||||||
|
this.$store.commit('playlist/UPDATE_PLAYLIST', this.$processPlaylist(
|
||||||
|
this.configPlayout.playlist.day_start, this.playlist))
|
||||||
|
|
||||||
|
const saveList = this.playlist.map(({ begin, ...item }) => item)
|
||||||
|
|
||||||
|
await this.$axios.post(
|
||||||
|
'api/player/playlist/',
|
||||||
|
{ data: { channel: this.$store.state.playlist.playlistChannel, date: this.listDate, program: saveList } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.control-container {
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
min-height: 254px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-col {
|
||||||
|
max-width: 542px;
|
||||||
|
min-width: 380px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-col {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 254px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-col {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-unit-col {
|
||||||
|
min-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-unit-row {
|
||||||
|
background: #32383E;
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-unit-row .col {
|
||||||
|
position: relative;
|
||||||
|
height: 50%;
|
||||||
|
min-height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-unit-row .col div {
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
-webkit-transform: translateY(-50%);
|
||||||
|
-ms-transform: translateY(-50%);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
font-size: 3em;
|
||||||
|
line-height: 0;
|
||||||
|
width: 80%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
height: 100%;
|
||||||
|
min-width: 370px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-col {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-col {
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-col {
|
||||||
|
position: relative;
|
||||||
|
background: #32383E;
|
||||||
|
padding: .5em;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-str {
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
-webkit-transform: translateY(-50%);
|
||||||
|
-ms-transform: translateY(-50%);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-family: 'DigitalNumbers-Regular';
|
||||||
|
font-size: 4.5em;
|
||||||
|
letter-spacing: -.18em;
|
||||||
|
padding-right: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-clip {
|
||||||
|
background: #32383E;
|
||||||
|
height: calc(50% - 3px);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-clip-text {
|
||||||
|
height: 70%;
|
||||||
|
padding-top: .5em;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-clip-progress {
|
||||||
|
top: 80%;
|
||||||
|
margin-top: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
background-image: linear-gradient(#3b4046, #2c3034 60%, #24272a) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button-play {
|
||||||
|
color: #43c32e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-playing {
|
||||||
|
box-shadow: 0 0 15px #43c32e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button-stop {
|
||||||
|
color: #d01111;
|
||||||
|
}
|
||||||
|
.control-button-reload {
|
||||||
|
color: #ed7c06;
|
||||||
|
}
|
||||||
|
.control-button-restart {
|
||||||
|
color: #f6e502;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1555px) {
|
||||||
|
.control-col {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 294px;
|
||||||
|
}
|
||||||
|
.status-col {
|
||||||
|
padding-right: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.time-str {
|
||||||
|
font-size: 3.5em;
|
||||||
|
}
|
||||||
|
.time-col {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.control-unit-row {
|
||||||
|
margin-right: -30px;
|
||||||
|
}
|
||||||
|
.control-unit-col {
|
||||||
|
flex: 0 0 66.6666666667%;
|
||||||
|
max-width: 66.6666666667%;
|
||||||
|
margin: 6px 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1225px) {
|
||||||
|
.clock-col {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-col {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-row {
|
||||||
|
height: 44px;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-row {
|
||||||
|
height: calc(100% - 40px - 254px - 46px - 70px);
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-row {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-div {
|
||||||
|
width: 250px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timecode {
|
||||||
|
min-width: 56px;
|
||||||
|
max-width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-input {
|
||||||
|
min-width: 35px;
|
||||||
|
max-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timecode input {
|
||||||
|
border-color: #515763;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item:nth-of-type(even), .playlist-item:nth-of-type(even) div .timecode input {
|
||||||
|
background-color: #3b424a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item:nth-of-type(even):hover {
|
||||||
|
background-color: #1C1E22;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
7
ffplayout/frontend/plugins/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# PLUGINS
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
|
21
ffplayout/frontend/plugins/axios.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default function ({ $axios, store, redirect }) {
|
||||||
|
$axios.onRequest((config) => {
|
||||||
|
const token = store.state.auth.jwtToken
|
||||||
|
if (token) {
|
||||||
|
config.headers.common.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable progress on auth and stats
|
||||||
|
if (config.url.includes('stats') || config.url.includes('auth') || config.url.includes('system')) {
|
||||||
|
config.progress = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$axios.onError((error) => {
|
||||||
|
const code = parseInt(error.response && error.response.status)
|
||||||
|
|
||||||
|
if (code === 400) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
5
ffplayout/frontend/plugins/draggable.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
|
Vue.use(draggable)
|
||||||
|
Vue.component('draggable', draggable)
|
20
ffplayout/frontend/plugins/filters.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
Vue.filter('toMin', function (sec) {
|
||||||
|
if (sec) {
|
||||||
|
const minutes = Math.floor(sec / 60)
|
||||||
|
const seconds = Math.round(sec - minutes * 60)
|
||||||
|
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')} min`
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Vue.filter('filename', function (path) {
|
||||||
|
if (path) {
|
||||||
|
const pathArr = path.split('/')
|
||||||
|
return pathArr[pathArr.length - 1]
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
34
ffplayout/frontend/plugins/helpers.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export default ({ app }, inject) => {
|
||||||
|
inject('processPlaylist', (dayStart, list) => {
|
||||||
|
const [h, m, s] = dayStart.split(':')
|
||||||
|
let begin = parseFloat(h) * 3600 + parseFloat(m) * 60 + parseFloat(s)
|
||||||
|
|
||||||
|
for (const item of list) {
|
||||||
|
item.begin = begin
|
||||||
|
|
||||||
|
if (!item.category) {
|
||||||
|
item.category = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
begin += (item.out - item.in)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
// convert time (00:00:00) string to seconds
|
||||||
|
inject('timeToSeconds', (time) => {
|
||||||
|
const t = time.split(':')
|
||||||
|
return parseInt(t[0]) * 3600 + parseInt(t[1]) * 60 + parseInt(t[2])
|
||||||
|
})
|
||||||
|
|
||||||
|
inject('secToHMS', (sec) => {
|
||||||
|
let hours = Math.floor(sec / 3600)
|
||||||
|
sec %= 3600
|
||||||
|
let minutes = Math.floor(sec / 60)
|
||||||
|
let seconds = sec % 60
|
||||||
|
|
||||||
|
minutes = String(minutes).padStart(2, '0')
|
||||||
|
hours = String(hours).padStart(2, '0')
|
||||||
|
seconds = String(parseInt(seconds)).padStart(2, '0')
|
||||||
|
return hours + ':' + minutes + ':' + seconds
|
||||||
|
})
|
||||||
|
}
|
6
ffplayout/frontend/plugins/loading.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Loading from 'vue-loading-overlay'
|
||||||
|
import 'vue-loading-overlay/dist/vue-loading.css'
|
||||||
|
|
||||||
|
Vue.use(Loading)
|
||||||
|
Vue.component('loading', Loading)
|
5
ffplayout/frontend/plugins/scrollbar.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import PerfectScrollbar from 'vue2-perfect-scrollbar'
|
||||||
|
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
|
||||||
|
|
||||||
|
Vue.use(PerfectScrollbar)
|
6
ffplayout/frontend/plugins/splitpanes.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { Splitpanes, Pane } from 'splitpanes'
|
||||||
|
import 'splitpanes/dist/splitpanes.css'
|
||||||
|
|
||||||
|
Vue.component('splitpanes', Splitpanes)
|
||||||
|
Vue.component('pane', Pane)
|
4
ffplayout/frontend/plugins/video.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VideoPlayer from '@/components/VideoPlayer.vue'
|
||||||
|
|
||||||
|
Vue.component('video-player', VideoPlayer)
|
11
ffplayout/frontend/static/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# STATIC
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your static files.
|
||||||
|
Each file inside this directory is mapped to `/`.
|
||||||
|
Thus you'd want to delete this README.md before deploying to production.
|
||||||
|
|
||||||
|
Example: `/static/robots.txt` is mapped as `/robots.txt`.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static).
|
BIN
ffplayout/frontend/static/favicon.ico
Normal file
After Width: | Height: | Size: 8.5 KiB |
2
ffplayout/frontend/static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
10
ffplayout/frontend/store/README.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# STORE
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your Vuex Store files.
|
||||||
|
Vuex Store option is implemented in the Nuxt.js framework.
|
||||||
|
|
||||||
|
Creating a file in this directory automatically activates the option in the framework.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
|
88
ffplayout/frontend/store/auth.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
import jwt_decode from 'jwt-decode'
|
||||||
|
|
||||||
|
export const state = () => ({
|
||||||
|
jwtToken: '',
|
||||||
|
jwtRefresh: '',
|
||||||
|
isLogin: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// mutate values in state
|
||||||
|
export const mutations = {
|
||||||
|
UPADTE_TOKEN (state, obj) {
|
||||||
|
state.jwtToken = obj.token
|
||||||
|
|
||||||
|
if (obj.refresh) {
|
||||||
|
state.jwtRefresh = obj.refresh
|
||||||
|
}
|
||||||
|
},
|
||||||
|
REMOVE_TOKEN (state) {
|
||||||
|
this.$cookies.remove('token')
|
||||||
|
this.$cookies.remove('refresh')
|
||||||
|
state.jwtToken = null
|
||||||
|
state.jwtRefresh = null
|
||||||
|
},
|
||||||
|
UPDATE_IS_LOGIN (state, bool) {
|
||||||
|
state.isLogin = bool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async obtainToken ({ commit, state }, { username, password }) {
|
||||||
|
const payload = {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
await this.$axios.post('auth/token/', payload)
|
||||||
|
.then((response) => {
|
||||||
|
commit('UPADTE_TOKEN', { token: response.data.access, refresh: response.data.refresh })
|
||||||
|
commit('UPDATE_IS_LOGIN', true)
|
||||||
|
this.$cookies.set('token', response.data.access, {
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 24 * 365
|
||||||
|
})
|
||||||
|
this.$cookies.set('refresh', response.data.refresh, {
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 24 * 365
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async refreshToken ({ commit, state }) {
|
||||||
|
const payload = {
|
||||||
|
refresh: state.jwtRefresh,
|
||||||
|
progress: false
|
||||||
|
}
|
||||||
|
const response = await this.$axios.post('auth/token/refresh/', payload)
|
||||||
|
|
||||||
|
commit('UPADTE_TOKEN', { token: response.data.access })
|
||||||
|
commit('UPDATE_IS_LOGIN', true)
|
||||||
|
},
|
||||||
|
|
||||||
|
async inspectToken ({ commit, dispatch, state }) {
|
||||||
|
const token = this.$cookies.get('token')
|
||||||
|
const refresh = this.$cookies.get('refresh')
|
||||||
|
|
||||||
|
if (token && refresh) {
|
||||||
|
commit('UPADTE_TOKEN', { token, refresh })
|
||||||
|
const decoded_token = jwt_decode(token)
|
||||||
|
const decoded_refresh = jwt_decode(refresh)
|
||||||
|
const timestamp = Date.now() / 1000
|
||||||
|
const expire_token = decoded_token.exp
|
||||||
|
const expire_refresh = decoded_refresh.exp
|
||||||
|
if (expire_token - timestamp > 0) {
|
||||||
|
// DO NOTHING, DO NOT REFRESH
|
||||||
|
commit('UPDATE_IS_LOGIN', true)
|
||||||
|
} else if (expire_refresh - timestamp > 0) {
|
||||||
|
await dispatch('refreshToken')
|
||||||
|
} else {
|
||||||
|
// PROMPT USER TO RE-LOGIN, THIS ELSE CLAUSE COVERS THE CONDITION WHERE A TOKEN IS EXPIRED AS WELL
|
||||||
|
commit('UPDATE_IS_LOGIN', false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commit('UPDATE_IS_LOGIN', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
ffplayout/frontend/store/config.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
export const state = () => ({
|
||||||
|
configGui: null,
|
||||||
|
netChoices: [],
|
||||||
|
configPlayout: null,
|
||||||
|
currentUser: null,
|
||||||
|
configUser: null
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
UPDATE_GUI_CONFIG (state, config) {
|
||||||
|
state.configGui = config
|
||||||
|
},
|
||||||
|
UPDATE_NET_CHOICES (state, list) {
|
||||||
|
state.netChoices = list
|
||||||
|
},
|
||||||
|
UPDATE_PLAYLOUT_CONFIG (state, config) {
|
||||||
|
state.configPlayout = config
|
||||||
|
},
|
||||||
|
SET_CURRENT_USER (state, user) {
|
||||||
|
state.currentUser = user
|
||||||
|
},
|
||||||
|
UPDATE_USER_CONFIG (state, config) {
|
||||||
|
state.configUser = config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async getGuiConfig ({ commit, state }) {
|
||||||
|
const options = await this.$axios.options('api/player/guisettings/')
|
||||||
|
const response = await this.$axios.get('api/player/guisettings/')
|
||||||
|
|
||||||
|
if (options.data) {
|
||||||
|
const choices = options.data.actions.POST.net_interface.choices.map(function (obj) {
|
||||||
|
obj.text = obj.display_name
|
||||||
|
delete obj.display_name
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
commit('UPDATE_NET_CHOICES', choices)
|
||||||
|
}
|
||||||
|
if (response.data) {
|
||||||
|
response.data[0].extra_extensions = response.data[0].extra_extensions.split(' ')
|
||||||
|
commit('UPDATE_GUI_CONFIG', response.data[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setGuiConfig ({ commit, state }, obj) {
|
||||||
|
const stringObj = JSON.parse(JSON.stringify(obj))
|
||||||
|
stringObj.extra_extensions = obj.extra_extensions.join(' ')
|
||||||
|
const update = await this.$axios.put('api/player/guisettings/1/', stringObj)
|
||||||
|
return update
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPlayoutConfig ({ commit, state }) {
|
||||||
|
const response = await this.$axios.get('api/player/config/?configPlayout')
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
commit('UPDATE_PLAYLOUT_CONFIG', response.data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setPlayoutConfig ({ commit, state }, obj) {
|
||||||
|
const update = await this.$axios.post('api/player/config/?configPlayout', { data: obj })
|
||||||
|
return update
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserConfig ({ commit, state }) {
|
||||||
|
const user = await this.$axios.get('api/player/user/current/')
|
||||||
|
const response = await this.$axios.get(`api/player/user/users/?username=${user.data.username}`)
|
||||||
|
|
||||||
|
if (user.data) {
|
||||||
|
commit('SET_CURRENT_USER', user.data.username)
|
||||||
|
}
|
||||||
|
if (response.data) {
|
||||||
|
commit('UPDATE_USER_CONFIG', response.data[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setUserConfig ({ commit, state }, obj) {
|
||||||
|
const update = await this.$axios.put(`api/player/user/users/${obj.id}/`, obj)
|
||||||
|
return update
|
||||||
|
}
|
||||||
|
}
|
1
ffplayout/frontend/store/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const strict = false
|
49
ffplayout/frontend/store/media.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
export const state = () => ({
|
||||||
|
currentPath: null,
|
||||||
|
crumbs: [],
|
||||||
|
folderTree: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
UPDATE_CURRENT_PATH (state, path) {
|
||||||
|
state.currentPath = path
|
||||||
|
},
|
||||||
|
UPDATE_CRUMBS (state, crumbs) {
|
||||||
|
state.crumbs = crumbs
|
||||||
|
},
|
||||||
|
UPDATE_FOLDER_TREE (state, tree) {
|
||||||
|
state.folderTree = tree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async getTree ({ commit, dispatch, state }, { extensions, path }) {
|
||||||
|
const crumbs = []
|
||||||
|
let root = '/'
|
||||||
|
const response = await this.$axios.get(`api/player/media/?extensions=${extensions}&path=${path}`)
|
||||||
|
|
||||||
|
if (response.data.tree) {
|
||||||
|
const pathArr = response.data.tree[0].split('/')
|
||||||
|
|
||||||
|
if (response.data.tree[1].length === 0) {
|
||||||
|
response.data.tree[1].push(pathArr[pathArr.length - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
for (const crumb of pathArr) {
|
||||||
|
if (crumb) {
|
||||||
|
root += crumb + '/'
|
||||||
|
crumbs.push({ text: crumb, path: root })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
crumbs.push({ text: pathArr[pathArr.length - 1], path: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(crumbs)
|
||||||
|
commit('UPDATE_CURRENT_PATH', path)
|
||||||
|
commit('UPDATE_CRUMBS', crumbs)
|
||||||
|
commit('UPDATE_FOLDER_TREE', response.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
ffplayout/frontend/store/playlist.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
function secToHMS (sec) {
|
||||||
|
let hours = Math.floor(sec / 3600)
|
||||||
|
sec %= 3600
|
||||||
|
let minutes = Math.floor(sec / 60)
|
||||||
|
let seconds = sec % 60
|
||||||
|
|
||||||
|
minutes = String(minutes).padStart(2, '0')
|
||||||
|
hours = String(hours).padStart(2, '0')
|
||||||
|
seconds = String(parseInt(seconds)).padStart(2, '0')
|
||||||
|
return hours + ':' + minutes + ':' + seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
export const state = () => ({
|
||||||
|
playlist: null,
|
||||||
|
playlistToday: [],
|
||||||
|
playlistChannel: 'Channel 1',
|
||||||
|
progressValue: 0,
|
||||||
|
currentClip: 'No clips is playing',
|
||||||
|
timeStr: '00:00:00',
|
||||||
|
timeLeft: '00:00:00'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
UPDATE_PLAYLIST (state, list) {
|
||||||
|
state.playlist = list
|
||||||
|
},
|
||||||
|
UPDATE_TODAYS_PLAYLIST (state, list) {
|
||||||
|
state.playlistToday = list
|
||||||
|
},
|
||||||
|
UPDATE_PLAYLIST_CHANNEL (state, channel) {
|
||||||
|
state.playlistChannel = channel
|
||||||
|
},
|
||||||
|
SET_PROGRESS_VALUE (state, value) {
|
||||||
|
state.progressValue = value
|
||||||
|
},
|
||||||
|
SET_CURRENT_CLIP (state, clip) {
|
||||||
|
state.currentClip = clip
|
||||||
|
},
|
||||||
|
SET_TIME (state, time) {
|
||||||
|
state.timeStr = time
|
||||||
|
},
|
||||||
|
SET_TIME_LEFT (state, time) {
|
||||||
|
state.timeLeft = time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async getPlaylist ({ commit, dispatch, state }, { dayStart, date }) {
|
||||||
|
const response = await this.$axios.get(`api/player/playlist/?date=${date}`)
|
||||||
|
|
||||||
|
if (response.data && response.data.program) {
|
||||||
|
commit('UPDATE_PLAYLIST_CHANNEL', response.data.channel)
|
||||||
|
commit('UPDATE_PLAYLIST', this.$processPlaylist(dayStart, response.data.program))
|
||||||
|
|
||||||
|
if (date === this.$dayjs().format('YYYY-MM-DD')) {
|
||||||
|
commit('UPDATE_TODAYS_PLAYLIST', JSON.parse(JSON.stringify(response.data.program)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commit('UPDATE_PLAYLIST', [])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
animClock ({ commit, dispatch, state }, { dayStart }) {
|
||||||
|
let start = this.$timeToSeconds(dayStart)
|
||||||
|
|
||||||
|
// loop over clips in program list from today
|
||||||
|
for (let i = 0; i < state.playlistToday.length; i++) {
|
||||||
|
const duration = state.playlistToday[i].out - state.playlistToday[i].in
|
||||||
|
const time = this.$dayjs().add(1, 'seconds').format('HH:mm:ss')
|
||||||
|
const playTime = this.$timeToSeconds(time) - start
|
||||||
|
|
||||||
|
// set current clip and progressbar value
|
||||||
|
if (playTime <= duration) {
|
||||||
|
commit('SET_CURRENT_CLIP', state.playlistToday[i].source)
|
||||||
|
commit('SET_PROGRESS_VALUE', playTime * 100 / duration)
|
||||||
|
commit('SET_TIME', time)
|
||||||
|
commit('SET_TIME_LEFT', secToHMS(duration - playTime))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
start += duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
ffplayout/manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE',
|
||||||
|
'ffplayout.settings.development')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
110
functions.php
@ -1,110 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// Include the DirectoryLister class
|
|
||||||
require_once('resources/DirectoryLister.php');
|
|
||||||
|
|
||||||
// Initialize the DirectoryLister object
|
|
||||||
$lister = new DirectoryLister();
|
|
||||||
|
|
||||||
// Initialize the directory array
|
|
||||||
if (isset($_GET['dir'])) {
|
|
||||||
$dirArray = $lister->listDirectory($_GET['dir']);
|
|
||||||
} else {
|
|
||||||
$dirArray = $lister->listDirectory('.');
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
if(!empty($_GET['head'])) {
|
|
||||||
$breadcrumbs = $lister->listBreadcrumbs(); ?>
|
|
||||||
<p class="navbar-text">
|
|
||||||
<?php foreach ($breadcrumbs as $breadcrumb): ?>
|
|
||||||
<?php if ($breadcrumb != end($breadcrumbs)): ?>
|
|
||||||
<a href="<?php echo $breadcrumb['link']; ?>" class="breadcumb"><?php echo $breadcrumb['text']; ?></a>
|
|
||||||
<span class="divider">/</span>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php echo $breadcrumb['text']; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</p>
|
|
||||||
<div id="directory-list-header">
|
|
||||||
<div class="row">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
if(!empty($_GET['ul'])) {
|
|
||||||
$get_type = $_GET['dir'];
|
|
||||||
|
|
||||||
foreach($dirArray as $name => $fileInfo):
|
|
||||||
if ($get_type === "ADtvMedia" and $name === "..") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if($fileInfo['icon_class'] === "fa-folder") {
|
|
||||||
$type = "folder";
|
|
||||||
} else if($fileInfo['icon_class'] === "fa-level-up") {
|
|
||||||
$type = "level";
|
|
||||||
} else {
|
|
||||||
$type = "file";
|
|
||||||
|
|
||||||
// file extension to filter
|
|
||||||
// for make it nicer
|
|
||||||
$except = array(
|
|
||||||
'avi',
|
|
||||||
'mp4',
|
|
||||||
'mov',
|
|
||||||
'mkv',
|
|
||||||
'mpg',
|
|
||||||
'mpeg',
|
|
||||||
);
|
|
||||||
|
|
||||||
$ext = implode('|', $except);
|
|
||||||
|
|
||||||
// formating filenames for the browser
|
|
||||||
if (preg_match('/^02 #/', $name) === 1) {
|
|
||||||
$name_exp = explode('#' , $name);
|
|
||||||
$name_end = preg_split("/[-.]/", $name_exp[4]);
|
|
||||||
$in_sec = $name_end[0] * 60 + $name_end[1];
|
|
||||||
$len = gmdate("H:i:s", $in_sec);
|
|
||||||
$name = $len . ' | ' . $name_exp[2] . '|' . $name_exp[3];
|
|
||||||
} elseif (preg_match_all('/#/', $name) === 2) {
|
|
||||||
$name_exp = explode('#' , $name);
|
|
||||||
$name_end = preg_split("/[-.]/", $name_exp[2]);
|
|
||||||
$in_sec = $name_end[0] * 60 + $name_end[1];
|
|
||||||
$len = gmdate("H:i:s", $in_sec);
|
|
||||||
$name = $len . ' | ' . $name_exp[0] . '|' . $name_exp[1];
|
|
||||||
} elseif (preg_match('/# [0-9-]+.('.$ext.')$/', $name) === 1) {
|
|
||||||
$name_exp = explode('#' , $name);
|
|
||||||
$name_end = preg_split("/[-.]/", end($name_exp));
|
|
||||||
$in_sec = $name_end[0] * 60 + $name_end[1];
|
|
||||||
$len = gmdate("H:i:s", $in_sec);
|
|
||||||
$name_pre = preg_replace('/# [0-9-]+.('.$ext.')$/', '', $name);
|
|
||||||
$name = $len . ' | ' . $name_pre;
|
|
||||||
}
|
|
||||||
$name = str_replace('§', '?', $name);
|
|
||||||
}?>
|
|
||||||
<li data-name="<?php echo $name; ?>" class="<?php echo $type ." ". $get_type ?>" data-href="<?php echo $fileInfo['url_path']; ?>">
|
|
||||||
<a href="<?php echo $fileInfo['url_path']; ?>" class="clearfix" data-name="<?php echo $name; ?>">
|
|
||||||
<div class="row">
|
|
||||||
<span class="file-name">
|
|
||||||
<i class="fa <?php echo $fileInfo['icon_class']; ?> fa-fw"></i>
|
|
||||||
<?php echo $name; ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<?php if (is_file($fileInfo['file_path'])): ?>
|
|
||||||
<a href="javascript:void(0)" class="file-info-button">
|
|
||||||
<i class="fa fa-play-circle"></i>
|
|
||||||
</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php if ($lister->containsIndex($fileInfo['file_path'])): ?>
|
|
||||||
<a href="<?php echo $fileInfo['file_path']; ?>" class="web-link-button" <?php if($lister->externalLinksNewWindow()): ?>target="_blank"<?php endif; ?>>
|
|
||||||
<i class="fa fa-external-link"></i>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach;
|
|
||||||
}
|
|
||||||
?>
|
|
106
index.html
@ -1,106 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>ffplayout</title>
|
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="">
|
|
||||||
<link rel="stylesheet" href="./resources/css/font-awesome.min.css">
|
|
||||||
<link rel="stylesheet" href="./resources/css/jquery-ui.min.css">
|
|
||||||
<link rel="stylesheet" href="./resources/css/style.css">
|
|
||||||
<link rel="stylesheet" href="./resources/css/video-js.min.css">
|
|
||||||
<link rel="stylesheet" href="./resources/css/pignose.calendar.css">
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta charset="utf-8">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header id="pageHeader">
|
|
||||||
<div id="headerCtr">
|
|
||||||
<div id="current-track" class="header-element">
|
|
||||||
<div id="track">
|
|
||||||
<div id="clock"></div>
|
|
||||||
<div id="countdown"></div>
|
|
||||||
<div id="title"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="streamer-ctr" class="header-element">
|
|
||||||
<div id=start-stop>
|
|
||||||
<div id="bt_start" class="fa fa-play-circle-o controll"></div>
|
|
||||||
<div id="bt_stop" class="fa fa-stop-circle-o controll"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<nav id="mainNav">
|
|
||||||
<div id="browser">
|
|
||||||
<div id="browserHead"></div>
|
|
||||||
<ul id="rootDirectory" class="nav nav-pills nav-stacked"></ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<article id="playlist-editor">
|
|
||||||
<ul id="playlistTable" class="inner-item">
|
|
||||||
<li class="row-start pad-left">Start</li>
|
|
||||||
<li class="row-file">File</li>
|
|
||||||
<li class="row-preview">Play</li>
|
|
||||||
<li class="row-duration">Duration</li>
|
|
||||||
<li class="row-in">In</li>
|
|
||||||
<li class="row-out">Out</li>
|
|
||||||
<li class="row-del-head">Delete</li>
|
|
||||||
</ul>
|
|
||||||
<div id="container">
|
|
||||||
<div id="list-container">
|
|
||||||
<ul id="playlistBody" class="list-group">
|
|
||||||
<li>...</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div id="cal">
|
|
||||||
<div class="calender"></div>
|
|
||||||
</div>
|
|
||||||
<div id="pagePlayer">
|
|
||||||
<video id="myStream" class="video-js vjs-default-skin vjs-16-9 vjs-big-play-centered" autoplay controls preload="auto">
|
|
||||||
<!-- change address to your server -->
|
|
||||||
<source src="rtmp://192.168.3.22/live/stream" type="rtmp/mp4">
|
|
||||||
<source src="live/stream.m3u8" type="application/x-mpegURL">
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
<footer id="pageFooter">
|
|
||||||
<div id="footer-area">
|
|
||||||
<div id="log-area" class="foot logging">
|
|
||||||
<div class="log-tabs">
|
|
||||||
<div id="bt_play" class="active logwindow">Playing</div>
|
|
||||||
<div id="bt_dec" class="logwindow">Decoder</div>
|
|
||||||
<div id="bt_enc" class="logwindow">Encoder</div>
|
|
||||||
<div id="bt_sys" class="logwindow">System</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="log-output" class="log-content">
|
|
||||||
<div id="output">ffplayout logging...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="playlistCtr" class="foot playlist-op">
|
|
||||||
<div class="list-save-reset">
|
|
||||||
<p>Playlist Operations</p>
|
|
||||||
<button id="bt_save" class="logwindow">Save</button>
|
|
||||||
<button id="bt_reset" class="logwindow">Reset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<div id="dialog-confirm" title=""></div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<!-- SCRIPTS -->
|
|
||||||
<script src="./resources/js/jquery.min.js"></script>
|
|
||||||
<script src="./resources/js/jquery-ui.min.js"></script>
|
|
||||||
<script src="./resources/js/Sortable.min.js"></script>
|
|
||||||
<script src="./resources/js/video.min.js"></script>
|
|
||||||
<script src="./resources/js/videojs-contrib-hls.min.js"></script>
|
|
||||||
<script src="./resources/js/videojs-flash.min.js"></script>
|
|
||||||
<script src="./resources/js/moment.min.js"></script>
|
|
||||||
<script src="./resources/js/pignose.calendar.js"></script>
|
|
||||||
<script src="./resources/js/custom.js"></script>
|
|
||||||
|
|
||||||
</html>
|
|
11
requirements-base.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Django<=3.1
|
||||||
|
django-filter
|
||||||
|
django-cors-headers
|
||||||
|
djangorestframework
|
||||||
|
djangorestframework-simplejwt
|
||||||
|
gunicorn
|
||||||
|
natsort
|
||||||
|
psutil
|
||||||
|
pymediainfo
|
||||||
|
pyyaml
|
||||||
|
zmq
|
16
requirements.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
asgiref==3.2.7
|
||||||
|
Django==3.0.5
|
||||||
|
django-cors-headers==3.2.1
|
||||||
|
django-filter==2.2.0
|
||||||
|
djangorestframework==3.11.0
|
||||||
|
djangorestframework-simplejwt==4.4.0
|
||||||
|
gunicorn==20.0.4
|
||||||
|
natsort==7.0.1
|
||||||
|
psutil==5.7.0
|
||||||
|
PyJWT==1.7.1
|
||||||
|
pymediainfo==4.1
|
||||||
|
pytz==2020.1
|
||||||
|
PyYAML==5.3.1
|
||||||
|
pyzmq==19.0.0
|
||||||
|
sqlparse==0.3.1
|
||||||
|
zmq==0.0.0
|
@ -1,738 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* A simple PHP based directory lister that lists the contents
|
|
||||||
* of a directory and all it's sub-directories and allows easy
|
|
||||||
* navigation of the files within.
|
|
||||||
*
|
|
||||||
* This software distributed under the MIT License
|
|
||||||
* http://www.opensource.org/licenses/mit-license.php
|
|
||||||
*
|
|
||||||
* More info available at http://www.directorylister.com
|
|
||||||
*
|
|
||||||
* @author Chris Kankiewicz (http://www.chriskankiewicz.com)
|
|
||||||
* @copyright 2015 Chris Kankiewicz
|
|
||||||
*/
|
|
||||||
class DirectoryLister {
|
|
||||||
|
|
||||||
// Define application version
|
|
||||||
const VERSION = '2.6.1';
|
|
||||||
|
|
||||||
// Reserve some variables
|
|
||||||
protected $_themeName = null;
|
|
||||||
protected $_directory = null;
|
|
||||||
protected $_appDir = null;
|
|
||||||
protected $_appURL = null;
|
|
||||||
protected $_config = null;
|
|
||||||
protected $_fileTypes = null;
|
|
||||||
protected $_systemMessage = null;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DirectoryLister construct function. Runs on object creation.
|
|
||||||
*/
|
|
||||||
public function __construct() {
|
|
||||||
|
|
||||||
// Set class directory constant
|
|
||||||
if(!defined('__DIR__')) {
|
|
||||||
define('__DIR__', dirname(__FILE__));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set application directory
|
|
||||||
$this->_appDir = __DIR__;
|
|
||||||
|
|
||||||
// Build the application URL
|
|
||||||
$this->_appURL = $this->_getAppUrl();
|
|
||||||
|
|
||||||
// Load the configuration file
|
|
||||||
$configFile = $this->_appDir . '/config.php';
|
|
||||||
|
|
||||||
// Set the config array to a global variable
|
|
||||||
if (file_exists($configFile)) {
|
|
||||||
$this->_config = include($configFile);
|
|
||||||
} else {
|
|
||||||
die('ERROR: Missing application config file at ' . $configFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the file types array to a global variable
|
|
||||||
$this->_fileTypes = require_once($this->_appDir . '/fileTypes.php');
|
|
||||||
|
|
||||||
// Set the theme name
|
|
||||||
$this->_themeName = $this->_config['theme_name'];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the directory listing and returns the formatted XHTML
|
|
||||||
*
|
|
||||||
* @param string $directory Relative path of directory to list
|
|
||||||
* @return array Array of directory being listed
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function listDirectory($directory) {
|
|
||||||
|
|
||||||
// Set directory
|
|
||||||
$directory = $this->setDirectoryPath($directory);
|
|
||||||
|
|
||||||
// Set directory variable if left blank
|
|
||||||
if ($directory === null) {
|
|
||||||
$directory = $this->_directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the directory array
|
|
||||||
$directoryArray = $this->_readDirectory($directory);
|
|
||||||
|
|
||||||
// Return the array
|
|
||||||
return $directoryArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and returns an array of breadcrumbs
|
|
||||||
*
|
|
||||||
* @param string $directory Path to be breadcrumbified
|
|
||||||
* @return array Array of breadcrumbs
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function listBreadcrumbs($directory = null) {
|
|
||||||
|
|
||||||
// Set directory variable if left blank
|
|
||||||
if ($directory === null) {
|
|
||||||
$directory = $this->_directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explode the path into an array
|
|
||||||
$dirArray = explode('/', $directory);
|
|
||||||
|
|
||||||
// Statically set the Home breadcrumb
|
|
||||||
/*
|
|
||||||
$breadcrumbsArray[] = array(
|
|
||||||
'link' => $this->_appURL,
|
|
||||||
'text' => 'Home'
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Generate breadcrumbs
|
|
||||||
foreach ($dirArray as $key => $dir) {
|
|
||||||
|
|
||||||
if ($dir != '.') {
|
|
||||||
|
|
||||||
$dirPath = null;
|
|
||||||
|
|
||||||
// Build the directory path
|
|
||||||
for ($i = 0; $i <= $key; $i++) {
|
|
||||||
$dirPath = $dirPath . $dirArray[$i] . '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove trailing slash
|
|
||||||
if(substr($dirPath, -1) == '/') {
|
|
||||||
$dirPath = substr($dirPath, 0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine the base path and dir path
|
|
||||||
$link = $this->_appURL . '?dir=' . rawurlencode($dirPath);
|
|
||||||
|
|
||||||
$breadcrumbsArray[] = array(
|
|
||||||
'link' => $link,
|
|
||||||
'text' => $dir
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the breadcrumb array
|
|
||||||
return $breadcrumbsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a directory contains an index file
|
|
||||||
*
|
|
||||||
* @param string $dirPath Path to directory to be checked for an index
|
|
||||||
* @return boolean Returns true if directory contains a valid index file, false if not
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function containsIndex($dirPath) {
|
|
||||||
|
|
||||||
// Check if directory contains an index file
|
|
||||||
foreach ($this->_config['index_files'] as $indexFile) {
|
|
||||||
|
|
||||||
if (file_exists($dirPath . '/' . $indexFile)) {
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get path of the listed directory
|
|
||||||
*
|
|
||||||
* @return string Path of the listed directory
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function getListedPath() {
|
|
||||||
|
|
||||||
// Build the path
|
|
||||||
if ($this->_directory == '.') {
|
|
||||||
$path = $this->_appURL;
|
|
||||||
} else {
|
|
||||||
$path = $this->_appURL . $this->_directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the path
|
|
||||||
return $path;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the theme name.
|
|
||||||
*
|
|
||||||
* @return string Theme name
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function getThemeName() {
|
|
||||||
// Return the theme name
|
|
||||||
return $this->_config['theme_name'];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns open links in another window
|
|
||||||
*
|
|
||||||
* @return boolean Returns true if in config is enabled open links in another window, false if not
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function externalLinksNewWindow() {
|
|
||||||
return $this->_config['external_links_new_window'];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the chosen theme directory
|
|
||||||
*
|
|
||||||
* @param bool $absolute Whether or not the path returned is absolute (default = false).
|
|
||||||
* @return string Path to theme
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function getThemePath($absolute = false) {
|
|
||||||
if ($absolute) {
|
|
||||||
// Set the theme path
|
|
||||||
$themePath = $this->_appDir . '/themes/' . $this->_themeName;
|
|
||||||
} else {
|
|
||||||
// Get relative path to application dir
|
|
||||||
$realtivePath = $this->_getRelativePath(getcwd(), $this->_appDir);
|
|
||||||
|
|
||||||
// Set the theme path
|
|
||||||
$themePath = $realtivePath . '/themes/' . $this->_themeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $themePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an array of error messages or false when empty
|
|
||||||
*
|
|
||||||
* @return array|bool Array of error messages or false
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function getSystemMessages() {
|
|
||||||
if (isset($this->_systemMessage) && is_array($this->_systemMessage)) {
|
|
||||||
return $this->_systemMessage;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set directory path variable
|
|
||||||
*
|
|
||||||
* @param string $path Path to directory
|
|
||||||
* @return string Sanitizd path to directory
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function setDirectoryPath($path = null) {
|
|
||||||
|
|
||||||
// Set the directory global variable
|
|
||||||
$this->_directory = $this->_setDirectoryPath($path);
|
|
||||||
|
|
||||||
return $this->_directory;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get directory path variable
|
|
||||||
*
|
|
||||||
* @return string Sanitizd path to directory
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function getDirectoryPath() {
|
|
||||||
return $this->_directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a message to the system message array
|
|
||||||
*
|
|
||||||
* @param string $type The type of message (ie - error, success, notice, etc.)
|
|
||||||
* @param string $message The message to be displayed to the user
|
|
||||||
* @return bool true on success
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function setSystemMessage($type, $text) {
|
|
||||||
|
|
||||||
// Create empty message array if it doesn't already exist
|
|
||||||
if (isset($this->_systemMessage) && !is_array($this->_systemMessage)) {
|
|
||||||
$this->_systemMessage = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the error message
|
|
||||||
$this->_systemMessage[] = array(
|
|
||||||
'type' => $type,
|
|
||||||
'text' => $text
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates and returns the directory path
|
|
||||||
*
|
|
||||||
* @param string $dir Directory path
|
|
||||||
* @return string Directory path to be listed
|
|
||||||
* @access protected
|
|
||||||
*/
|
|
||||||
protected function _setDirectoryPath($dir) {
|
|
||||||
|
|
||||||
// Check for an empty variable
|
|
||||||
if (empty($dir) || $dir == '.') {
|
|
||||||
return '.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eliminate double slashes
|
|
||||||
while (strpos($dir, '//')) {
|
|
||||||
$dir = str_replace('//', '/', $dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove trailing slash if present
|
|
||||||
if(substr($dir, -1, 1) == '/') {
|
|
||||||
$dir = substr($dir, 0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify file path exists and is a directory
|
|
||||||
if (!file_exists($dir) || !is_dir($dir)) {
|
|
||||||
// Set the error message
|
|
||||||
$this->setSystemMessage('danger', '<b>ERROR:</b> File path does not exist');
|
|
||||||
|
|
||||||
// Return the web root
|
|
||||||
return '.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent access to hidden files
|
|
||||||
if ($this->_isHidden($dir)) {
|
|
||||||
// Set the error message
|
|
||||||
$this->setSystemMessage('danger', '<b>ERROR:</b> Access denied');
|
|
||||||
|
|
||||||
// Set the directory to web root
|
|
||||||
return '.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent access to parent folders
|
|
||||||
if (strpos($dir, '<') !== false || strpos($dir, '>') !== false
|
|
||||||
|| strpos($dir, '..') !== false || strpos($dir, '/') === 0) {
|
|
||||||
// Set the error message
|
|
||||||
$this->setSystemMessage('danger', '<b>ERROR:</b> An invalid path string was detected');
|
|
||||||
|
|
||||||
// Set the directory to web root
|
|
||||||
return '.';
|
|
||||||
} else {
|
|
||||||
// Should stop all URL wrappers (Thanks to Hexatex)
|
|
||||||
$directoryPath = $dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return
|
|
||||||
return $directoryPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loop through directory and return array with file info, including
|
|
||||||
* file path, icon and sort order.
|
|
||||||
*
|
|
||||||
* @param string $directory Directory path
|
|
||||||
* @param string $sort Sort method (default = natcase)
|
|
||||||
* @return array Array of the directory contents
|
|
||||||
* @access protected
|
|
||||||
*/
|
|
||||||
protected function _readDirectory($directory, $sort = 'natcase') {
|
|
||||||
|
|
||||||
// Initialize array
|
|
||||||
$directoryArray = array();
|
|
||||||
|
|
||||||
// Get directory contents
|
|
||||||
$files = scandir($directory);
|
|
||||||
|
|
||||||
// Read files/folders from the directory
|
|
||||||
foreach ($files as $file) {
|
|
||||||
|
|
||||||
if ($file != '.') {
|
|
||||||
|
|
||||||
// Get files relative path
|
|
||||||
$relativePath = $directory . '/' . $file;
|
|
||||||
|
|
||||||
if (substr($relativePath, 0, 2) == './') {
|
|
||||||
$relativePath = substr($relativePath, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't check parent dir if we're in the root dir
|
|
||||||
if ($this->_directory == '.' && $file == '..'){
|
|
||||||
|
|
||||||
continue;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
$realPath = realpath($relativePath);
|
|
||||||
|
|
||||||
// Determine file type by extension
|
|
||||||
if (is_dir($realPath)) {
|
|
||||||
$iconClass = 'fa-folder';
|
|
||||||
$sort = 1;
|
|
||||||
} else {
|
|
||||||
// Get file extension
|
|
||||||
$fileExt = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
|
|
||||||
|
|
||||||
if (isset($this->_fileTypes[$fileExt])) {
|
|
||||||
$iconClass = $this->_fileTypes[$fileExt];
|
|
||||||
} else {
|
|
||||||
$iconClass = $this->_fileTypes['blank'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sort = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($file == '..') {
|
|
||||||
|
|
||||||
if ($this->_directory != '.') {
|
|
||||||
// Get parent directory path
|
|
||||||
$pathArray = explode('/', $relativePath);
|
|
||||||
unset($pathArray[count($pathArray)-1]);
|
|
||||||
unset($pathArray[count($pathArray)-1]);
|
|
||||||
$directoryPath = implode('/', $pathArray);
|
|
||||||
|
|
||||||
if (!empty($directoryPath)) {
|
|
||||||
$directoryPath = '?dir=' . rawurlencode($directoryPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add file info to the array
|
|
||||||
$directoryArray['..'] = array(
|
|
||||||
'file_path' => $this->_appURL . $directoryPath,
|
|
||||||
'url_path' => $this->_appURL . $directoryPath,
|
|
||||||
'icon_class' => 'fa-level-up',
|
|
||||||
'sort' => 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
} elseif (!$this->_isHidden($relativePath)) {
|
|
||||||
|
|
||||||
// Add all non-hidden files to the array
|
|
||||||
if ($this->_directory != '.' || $file != 'index.php') {
|
|
||||||
|
|
||||||
// Build the file path
|
|
||||||
$urlPath = implode('/', array_map('rawurlencode', explode('/', $relativePath)));
|
|
||||||
|
|
||||||
if (is_dir($relativePath)) {
|
|
||||||
$urlPath = '?dir=' . $urlPath;
|
|
||||||
} else {
|
|
||||||
$urlPath = $urlPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the info to the main array
|
|
||||||
$directoryArray[pathinfo($relativePath, PATHINFO_BASENAME)] = array(
|
|
||||||
'file_path' => $relativePath,
|
|
||||||
'url_path' => $urlPath,
|
|
||||||
'icon_class' => $iconClass,
|
|
||||||
'sort' => $sort
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the array
|
|
||||||
$reverseSort = in_array($this->_directory, $this->_config['reverse_sort']);
|
|
||||||
$sortedArray = $this->_arraySort($directoryArray, $this->_config['list_sort_order'], $reverseSort);
|
|
||||||
|
|
||||||
// Return the array
|
|
||||||
return $sortedArray;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts an array by the provided sort method.
|
|
||||||
*
|
|
||||||
* @param array $array Array to be sorted
|
|
||||||
* @param string $sortMethod Sorting method (acceptable inputs: natsort, natcasesort, etc.)
|
|
||||||
* @param boolen $reverse Reverse the sorted array order if true (default = false)
|
|
||||||
* @return array
|
|
||||||
* @access protected
|
|
||||||
*/
|
|
||||||
protected function _arraySort($array, $sortMethod, $reverse = false) {
|
|
||||||
// Create empty arrays
|
|
||||||
$sortedArray = array();
|
|
||||||
$finalArray = array();
|
|
||||||
|
|
||||||
// Create new array of just the keys and sort it
|
|
||||||
$keys = array_keys($array);
|
|
||||||
|
|
||||||
switch ($sortMethod) {
|
|
||||||
case 'asort':
|
|
||||||
asort($keys);
|
|
||||||
break;
|
|
||||||
case 'arsort':
|
|
||||||
arsort($keys);
|
|
||||||
break;
|
|
||||||
case 'ksort':
|
|
||||||
ksort($keys);
|
|
||||||
break;
|
|
||||||
case 'krsort':
|
|
||||||
krsort($keys);
|
|
||||||
break;
|
|
||||||
case 'natcasesort':
|
|
||||||
natcasesort($keys);
|
|
||||||
break;
|
|
||||||
case 'natsort':
|
|
||||||
natsort($keys);
|
|
||||||
break;
|
|
||||||
case 'shuffle':
|
|
||||||
shuffle($keys);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through the sorted values and move over the data
|
|
||||||
if ($this->_config['list_folders_first']) {
|
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
if ($array[$key]['sort'] == 0) {
|
|
||||||
$sortedArray['0'][$key] = $array[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
if ($array[$key]['sort'] == 1) {
|
|
||||||
$sortedArray[1][$key] = $array[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
if ($array[$key]['sort'] == 2) {
|
|
||||||
$sortedArray[2][$key] = $array[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($reverse) {
|
|
||||||
$sortedArray[1] = array_reverse($sortedArray[1]);
|
|
||||||
$sortedArray[2] = array_reverse($sortedArray[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
if ($array[$key]['sort'] == 0) {
|
|
||||||
$sortedArray[0][$key] = $array[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
if ($array[$key]['sort'] > 0) {
|
|
||||||
$sortedArray[1][$key] = $array[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($reverse) {
|
|
||||||
$sortedArray[1] = array_reverse($sortedArray[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge the arrays
|
|
||||||
foreach ($sortedArray as $array) {
|
|
||||||
if (empty($array)) continue;
|
|
||||||
foreach ($array as $key => $value) {
|
|
||||||
$finalArray[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return sorted array
|
|
||||||
return $finalArray;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a file is specified as hidden
|
|
||||||
*
|
|
||||||
* @param string $filePath Path to file to be checked if hidden
|
|
||||||
* @return boolean Returns true if file is in hidden array, false if not
|
|
||||||
* @access protected
|
|
||||||
*/
|
|
||||||
protected function _isHidden($filePath) {
|
|
||||||
|
|
||||||
// Add dot files to hidden files array
|
|
||||||
if ($this->_config['hide_dot_files']) {
|
|
||||||
|
|
||||||
$this->_config['hidden_files'] = array_merge(
|
|
||||||
$this->_config['hidden_files'],
|
|
||||||
array('.*', '*/.*')
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare path array to all hidden file paths
|
|
||||||
foreach ($this->_config['hidden_files'] as $hiddenPath) {
|
|
||||||
|
|
||||||
if (fnmatch($hiddenPath, $filePath)) {
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the root application URL from server variables.
|
|
||||||
*
|
|
||||||
* @return string The application URL
|
|
||||||
* @access protected
|
|
||||||
*/
|
|
||||||
protected function _getAppUrl() {
|
|
||||||
|
|
||||||
// Get the server protocol
|
|
||||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
|
||||||
$protocol = 'https://';
|
|
||||||
} else {
|
|
||||||
$protocol = 'http://';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the server hostname
|
|
||||||
$host = $_SERVER['HTTP_HOST'];
|
|
||||||
|
|
||||||
// Get the URL path
|
|
||||||
$pathParts = pathinfo($_SERVER['PHP_SELF']);
|
|
||||||
$path = $pathParts['dirname'];
|
|
||||||
|
|
||||||
// Remove backslash from path (Windows fix)
|
|
||||||
if (substr($path, -1) == '\\') {
|
|
||||||
$path = substr($path, 0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the path ends with a forward slash
|
|
||||||
if (substr($path, -1) != '/') {
|
|
||||||
$path = $path . '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the application URL
|
|
||||||
$appUrl = $protocol . $host . $path;
|
|
||||||
|
|
||||||
// Return the URL
|
|
||||||
return $appUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two paths and returns the relative path from one to the other
|
|
||||||
*
|
|
||||||
* @param string $fromPath Starting path
|
|
||||||
* @param string $toPath Ending path
|
|
||||||
* @return string $relativePath Relative path from $fromPath to $toPath
|
|
||||||
* @access protected
|
|
||||||
*/
|
|
||||||
protected function _getRelativePath($fromPath, $toPath) {
|
|
||||||
|
|
||||||
// Define the OS specific directory separator
|
|
||||||
if (!defined('DS')) define('DS', DIRECTORY_SEPARATOR);
|
|
||||||
|
|
||||||
// Remove double slashes from path strings
|
|
||||||
$fromPath = str_replace(DS . DS, DS, $fromPath);
|
|
||||||
$toPath = str_replace(DS . DS, DS, $toPath);
|
|
||||||
|
|
||||||
// Explode working dir and cache dir into arrays
|
|
||||||
$fromPathArray = explode(DS, $fromPath);
|
|
||||||
$toPathArray = explode(DS, $toPath);
|
|
||||||
|
|
||||||
// Remove last fromPath array element if it's empty
|
|
||||||
$x = count($fromPathArray) - 1;
|
|
||||||
|
|
||||||
if(!trim($fromPathArray[$x])) {
|
|
||||||
array_pop($fromPathArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove last toPath array element if it's empty
|
|
||||||
$x = count($toPathArray) - 1;
|
|
||||||
|
|
||||||
if(!trim($toPathArray[$x])) {
|
|
||||||
array_pop($toPathArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get largest array count
|
|
||||||
$arrayMax = max(count($fromPathArray), count($toPathArray));
|
|
||||||
|
|
||||||
// Set some default variables
|
|
||||||
$diffArray = array();
|
|
||||||
$samePath = true;
|
|
||||||
$key = 1;
|
|
||||||
|
|
||||||
// Generate array of the path differences
|
|
||||||
while ($key <= $arrayMax) {
|
|
||||||
|
|
||||||
// Get to path variable
|
|
||||||
$toPath = isset($toPathArray[$key]) ? $toPathArray[$key] : null;
|
|
||||||
|
|
||||||
// Get from path variable
|
|
||||||
$fromPath = isset($fromPathArray[$key]) ? $fromPathArray[$key] : null;
|
|
||||||
|
|
||||||
if ($toPath !== $fromPath || $samePath !== true) {
|
|
||||||
|
|
||||||
// Prepend '..' for every level up that must be traversed
|
|
||||||
if (isset($fromPathArray[$key])) {
|
|
||||||
array_unshift($diffArray, '..');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append directory name for every directory that must be traversed
|
|
||||||
if (isset($toPathArray[$key])) {
|
|
||||||
$diffArray[] = $toPathArray[$key];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directory paths have diverged
|
|
||||||
$samePath = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment key
|
|
||||||
$key++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the relative thumbnail directory path
|
|
||||||
$relativePath = implode('/', $diffArray);
|
|
||||||
|
|
||||||
// Return the relative path
|
|
||||||
return $relativePath;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
return array(
|
|
||||||
|
|
||||||
// Basic settings
|
|
||||||
'hide_dot_files' => true,
|
|
||||||
'list_folders_first' => true,
|
|
||||||
'list_sort_order' => 'natcasesort',
|
|
||||||
'theme_name' => 'bootstrap',
|
|
||||||
'external_links_new_window' => true,
|
|
||||||
|
|
||||||
// Hidden files
|
|
||||||
'hidden_files' => array(
|
|
||||||
'.ht*',
|
|
||||||
'*/.ht*',
|
|
||||||
'resources',
|
|
||||||
'resources/*',
|
|
||||||
'live',
|
|
||||||
'live/*',
|
|
||||||
'functions.php',
|
|
||||||
'process.sh',
|
|
||||||
'info.php',
|
|
||||||
'ffplayout-gui.png',
|
|
||||||
'README.md',
|
|
||||||
'.gitignore',
|
|
||||||
'.git',
|
|
||||||
'.git/*',
|
|
||||||
),
|
|
||||||
|
|
||||||
// Files that, if present in a directory, make the directory
|
|
||||||
// a direct link rather than a browse link.
|
|
||||||
'index_files' => array(
|
|
||||||
'index.htm',
|
|
||||||
'index.html',
|
|
||||||
'index.php'
|
|
||||||
),
|
|
||||||
|
|
||||||
// Custom sort order
|
|
||||||
'reverse_sort' => array(
|
|
||||||
// 'path/to/folder'
|
|
||||||
),
|
|
||||||
);
|
|
4
resources/css/font-awesome.min.css
vendored
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 6.2 KiB |
7
resources/css/jquery-ui.min.css
vendored
5
resources/css/jquery-ui.structure.min.css
vendored
5
resources/css/jquery-ui.theme.min.css
vendored
@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
* CSS Styles that are needed by jScrollPane for it to operate correctly.
|
|
||||||
*
|
|
||||||
* Include this stylesheet in your site or copy and paste the styles below into your stylesheet - jScrollPane
|
|
||||||
* may not operate correctly without them.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
.scroll-pane {
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.jspContainer
|
|
||||||
{
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspPane
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspVerticalBar
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 10px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
.jspHorizontalBar
|
|
||||||
{
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 16px;
|
|
||||||
background: black;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.jspTrack
|
|
||||||
{
|
|
||||||
background: #4d4c4c;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspDrag
|
|
||||||
{
|
|
||||||
background: #0d0d0d;
|
|
||||||
position: relative;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspHorizontalBar .jspTrack,
|
|
||||||
.jspHorizontalBar .jspDrag
|
|
||||||
{
|
|
||||||
float: left;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspArrow
|
|
||||||
{
|
|
||||||
background: #50506d;
|
|
||||||
text-indent: -20000px;
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspArrow.jspDisabled
|
|
||||||
{
|
|
||||||
cursor: default;
|
|
||||||
background: #80808d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspVerticalBar .jspArrow
|
|
||||||
{
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspHorizontalBar .jspArrow
|
|
||||||
{
|
|
||||||
width: 16px;
|
|
||||||
float: left;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspVerticalBar .jspArrow:focus
|
|
||||||
{
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jspCorner
|
|
||||||
{
|
|
||||||
background: #eeeef4;
|
|
||||||
float: left;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Yuk! CSS Hack for IE6 3 pixel bug :( */
|
|
||||||
* html .jspCorner
|
|
||||||
{
|
|
||||||
margin: 0 -3px 0 0;
|
|
||||||
}
|
|
@ -1,380 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'pignose-calendar-icon';
|
|
||||||
src: url("../fonts/pignose.calendar.eot?gpa4vl");
|
|
||||||
src: url("../fonts/pignose.calendar.eot?gpa4vl#iefix") format('embedded-opentype'), url("../fonts/pignose.calendar.ttf?gpa4vl") format('truetype'), url("../fonts/pignose.calendar.woff?gpa4vl") format('woff'), url("../fonts/pignose.calendar.svg?gpa4vl#pignose.calendar") format('svg');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
.pignose-calendar .icon-arrow-left,
|
|
||||||
.pignose-calendar .icon-arrow-right {
|
|
||||||
font-family: 'pignose-calendar-icon' !important;
|
|
||||||
speak: none;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
font-variant: normal;
|
|
||||||
text-transform: none;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.pignose-calendar .icon-arrow-left:before {
|
|
||||||
content: '\e90b';
|
|
||||||
}
|
|
||||||
.pignose-calendar .icon-arrow-right:before {
|
|
||||||
content: '\e90a';
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 360px;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
z-index: 50001;
|
|
||||||
overflow: hidden;
|
|
||||||
transform: translate3d(0, 160px, 0);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease, transform 0.5s ease-out;
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper.pignose-calendar-wrapper-active {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper .pignose-calendar {
|
|
||||||
max-width: auto;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper .pignose-calendar .pignose-calendar-button-group {
|
|
||||||
border-top: 1px solid #e2e2e2;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper .pignose-calendar .pignose-calendar-button-group .pignose-calendar-button {
|
|
||||||
width: 50%;
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
height: 3.2em;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 3.2em;
|
|
||||||
color: #333333;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper .pignose-calendar .pignose-calendar-button-group .pignose-calendar-button:hover {
|
|
||||||
background-color: #efefef;
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper .pignose-calendar .pignose-calendar-button-group .pignose-calendar-button-apply {
|
|
||||||
color: #111111;
|
|
||||||
background-color: #2fabb7;
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper .pignose-calendar .pignose-calendar-button-group .pignose-calendar-button-apply:hover {
|
|
||||||
background-color: #49c4d0;
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper-overlay {
|
|
||||||
background-color: #000000;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .3s ease;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 50000;
|
|
||||||
}
|
|
||||||
.pignose-calendar-wrapper-overlay.pignose-calendar-wrapper-overlay-active {
|
|
||||||
opacity: .7;
|
|
||||||
}
|
|
||||||
.pignose-calendar {
|
|
||||||
width: 90%;
|
|
||||||
max-width: 360px;
|
|
||||||
background-color: #111111;
|
|
||||||
font-size: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top {
|
|
||||||
padding: 2.6em 0;
|
|
||||||
background-color: #fafafa;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-date {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 1.8em 0;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-year,
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-month {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-year {
|
|
||||||
font-size: 115%;
|
|
||||||
color: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-month {
|
|
||||||
margin-bottom: .4em;
|
|
||||||
font-size: 130%;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-nav {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1.6em;
|
|
||||||
height: 1.6em;
|
|
||||||
position: relative;
|
|
||||||
z-index: 5;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-nav .pignose-calendar-top-value {
|
|
||||||
display: inline-block;
|
|
||||||
color: #777777;
|
|
||||||
font-size: 115%;
|
|
||||||
font-weight: 600;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-top: -10px;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-nav .pignose-calendar-top-icon {
|
|
||||||
color: #555555;
|
|
||||||
font-size: 160%;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-nav.pignose-calendar-top-prev {
|
|
||||||
float: left;
|
|
||||||
margin-left: 1.6em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-nav.pignose-calendar-top-prev .pignose-calendar-top-value {
|
|
||||||
margin-left: .2em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-nav.pignose-calendar-top-next {
|
|
||||||
float: right;
|
|
||||||
margin-right: 1.6em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-top .pignose-calendar-top-nav.pignose-calendar-top-next .pignose-calendar-top-value {
|
|
||||||
margin-right: .2em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-header {
|
|
||||||
padding: 0 1.2em;
|
|
||||||
margin-top: 1.2em;
|
|
||||||
font-weight: 600;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-header .pignose-calendar-week {
|
|
||||||
float: left;
|
|
||||||
width: 14.28%;
|
|
||||||
height: 2.8em;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 2.8em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-header .pignose-calendar-week.pignose-calendar-week-sun,
|
|
||||||
.pignose-calendar .pignose-calendar-header .pignose-calendar-week.pignose-calendar-week-sat {
|
|
||||||
color: #fa4832;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-header .pignose-calendar-week:last-child {
|
|
||||||
width: 14.32%;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-body {
|
|
||||||
padding: 1.2em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-body .pignose-calendar-row {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit {
|
|
||||||
float: left;
|
|
||||||
display: block;
|
|
||||||
height: 3.8em;
|
|
||||||
width: 14.28%;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 2.8em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit:last-child {
|
|
||||||
width: 14.32%;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit .pignose-calendar-button-schedule-container {
|
|
||||||
line-height: .5em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit .pignose-calendar-button-schedule-container .pignose-calendar-button-schedule-pin {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #777777;
|
|
||||||
width: .5em;
|
|
||||||
height: .5em;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: .2em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit .pignose-calendar-button-schedule-container .pignose-calendar-button-schedule-pin:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit a {
|
|
||||||
display: inline-block;
|
|
||||||
width: 2.4em;
|
|
||||||
height: 2.4em;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #49c4d0;
|
|
||||||
line-height: 2.4em;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit a:active {
|
|
||||||
background-color: #d8d8d8;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-disabled a {
|
|
||||||
opacity: .5;
|
|
||||||
background-color: #efefef;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-active a {
|
|
||||||
background-color: #2fabb7;
|
|
||||||
color: #111111;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-active.pignose-calendar-unit-sun a,
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-active.pignose-calendar-unit-sat a {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-range a {
|
|
||||||
background-color: #efefef;
|
|
||||||
border-radius: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-range.pignose-calendar-unit-disabled a {
|
|
||||||
color: #b2b9bb;
|
|
||||||
background-color: #e1e1e1;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-range.pignose-calendar-unit-range-first a {
|
|
||||||
border-top-left-radius: 1.2em;
|
|
||||||
border-bottom-left-radius: 1.2em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-range.pignose-calendar-unit-range-last a {
|
|
||||||
border-top-right-radius: 1.2em;
|
|
||||||
border-bottom-right-radius: 1.2em;
|
|
||||||
}
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-sun a,
|
|
||||||
.pignose-calendar .pignose-calendar-unit.pignose-calendar-unit-sat a {
|
|
||||||
color: #fa4832;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-default .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-toggle-active a {
|
|
||||||
color: #cccccc !important;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-default.pignose-calendar-reverse .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-toggle-inactive a {
|
|
||||||
color: #cccccc !important;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark {
|
|
||||||
background-color: #b0b0b0;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-top {
|
|
||||||
background-color: #8c8c8c;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-top .pignose-calendar-top-month {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-top .pignose-calendar-top-year {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-top .pignose-calendar-top-nav .pignose-calendar-top-value {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-top .pignose-calendar-top-nav .pignose-calendar-top-icon {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-header .pignose-calendar-week {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-header.pignose-calendar-week-sun,
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-header.pignose-calendar-week-sat {
|
|
||||||
color: #444444;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit a {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-sun a,
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-sat a {
|
|
||||||
color: #444444;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-disabled a {
|
|
||||||
color: #868e8f;
|
|
||||||
background-color: #5d6365;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-active a {
|
|
||||||
color: #111111;
|
|
||||||
background-color: #B38E8E;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-toggle a {
|
|
||||||
color: #8b8f94;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-range a {
|
|
||||||
background-color: #5a5d62;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-range.pignose-calendar-unit-disabled a {
|
|
||||||
color: #727a7c;
|
|
||||||
background-color: #4f5558;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-button-group {
|
|
||||||
border-top: 1px solid #323537;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-button-group .pignose-calendar-button {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-button-group .pignose-calendar-button:hover {
|
|
||||||
background-color: #5a5d62;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-dark .pignose-calendar-button-group .pignose-calendar-button-apply {
|
|
||||||
color: #111111;
|
|
||||||
background-color: #B38E8E;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue {
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-top {
|
|
||||||
background-color: #009fe3;
|
|
||||||
border-bottom-color: #e1e1e1;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-top .pignose-calendar-top-month {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-top .pignose-calendar-top-year {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-top .pignose-calendar-top-nav .pignose-calendar-top-value {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-top .pignose-calendar-top-nav .pignose-calendar-top-icon {
|
|
||||||
color: #111111;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-header .pignose-calendar-week {
|
|
||||||
color: #5c6270;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-header .pignose-calendar-week.pignose-calendar-week-sun,
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-header .pignose-calendar-week.pignose-calendar-week-sat {
|
|
||||||
color: #fa4832;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit a {
|
|
||||||
color: #5c6270;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-sun a,
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-sat a {
|
|
||||||
color: #fa4832;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-disabled a {
|
|
||||||
background-color: #efefef;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-active a {
|
|
||||||
color: #111111;
|
|
||||||
background-color: #009fe3;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-toggle a {
|
|
||||||
color: #cccccc;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-range a {
|
|
||||||
background-color: #efefef;
|
|
||||||
}
|
|
||||||
.pignose-calendar.pignose-calendar-blue .pignose-calendar-body .pignose-calendar-row .pignose-calendar-unit.pignose-calendar-unit-range.pignose-calendar-unit-disabled a {
|
|
||||||
background-color: #efefef;
|
|
||||||
}
|
|