Compare commits
4 Commits
4ea27fa76c
...
f195afa0c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f195afa0c9 | ||
|
|
4914d840d5 | ||
|
|
c731ca81c1 | ||
|
|
0974b33785 |
BIN
Tv-Anunturi-InfoBeamer-Package.zip
Normal file
BIN
Tv-Anunturi-InfoBeamer-Package.zip
Normal file
Binary file not shown.
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"server_url": "http://192.168.1.22",
|
"server_url": "http://192.168.1.22",
|
||||||
"player_id": "REPLACE_WITH_YOUR_DEVICE_ID",
|
"player_id": "12131415",
|
||||||
"refresh_interval": 30,
|
"refresh_interval": 30,
|
||||||
"default_duration": 10
|
"default_duration": 10,
|
||||||
|
"touch_enabled": true,
|
||||||
|
"swipe_threshold": 100,
|
||||||
|
"manual_override_duration": 10
|
||||||
}
|
}
|
||||||
|
|||||||
34
deployment-package/README.md
Normal file
34
deployment-package/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Info-Beamer Package for Tv-Anunturi (ID: 12131415)
|
||||||
|
|
||||||
|
## Files:
|
||||||
|
- node.lua (main script)
|
||||||
|
- config.json (configured for your player)
|
||||||
|
- roboto.ttf (font file)
|
||||||
|
- wpa_supplicant.conf (WiFi configuration template)
|
||||||
|
- configure-wifi.sh (WiFi setup script)
|
||||||
|
- network-config.txt (network configuration guide)
|
||||||
|
|
||||||
|
## WiFi Setup:
|
||||||
|
1. Run: ./configure-wifi.sh
|
||||||
|
2. Enter your WiFi network name and password
|
||||||
|
3. Copy generated wpa_supplicant.conf to SD card /boot/ folder
|
||||||
|
|
||||||
|
## Deployment:
|
||||||
|
1. Configure WiFi (see above)
|
||||||
|
2. Copy package files to SD card root: node.lua, config.json, roboto.ttf
|
||||||
|
3. Copy wpa_supplicant.conf to SD card /boot/ folder
|
||||||
|
4. Insert SD card and power on device
|
||||||
|
5. Device will connect to WiFi and server at 192.168.1.22
|
||||||
|
|
||||||
|
## Features:
|
||||||
|
- Touch screen swipe navigation (left/right)
|
||||||
|
- Automatic content updates every 30 seconds
|
||||||
|
- Shows content from Default Channel
|
||||||
|
- Manual override with touch control
|
||||||
|
|
||||||
|
## Touch Usage:
|
||||||
|
- Swipe right = next content
|
||||||
|
- Swipe left = previous content
|
||||||
|
- Auto-resume after 10 seconds
|
||||||
|
|
||||||
|
Server: http://192.168.1.22/admin
|
||||||
9
deployment-package/config.json
Normal file
9
deployment-package/config.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"server_url": "http://192.168.1.22",
|
||||||
|
"player_id": "12131415",
|
||||||
|
"refresh_interval": 30,
|
||||||
|
"default_duration": 10,
|
||||||
|
"touch_enabled": true,
|
||||||
|
"swipe_threshold": 100,
|
||||||
|
"manual_override_duration": 10
|
||||||
|
}
|
||||||
44
deployment-package/configure-wifi.sh
Executable file
44
deployment-package/configure-wifi.sh
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# WiFi Configuration Script for Info-Beamer Package
|
||||||
|
|
||||||
|
echo "=== Info-Beamer WiFi Configuration ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get WiFi network name
|
||||||
|
read -p "Enter WiFi Network Name (SSID): " WIFI_SSID
|
||||||
|
|
||||||
|
# Get WiFi password
|
||||||
|
read -s -p "Enter WiFi Password: " WIFI_PASSWORD
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get country code
|
||||||
|
echo ""
|
||||||
|
echo "Common country codes: US, GB, DE, FR, CA, AU, JP"
|
||||||
|
read -p "Enter country code [US]: " COUNTRY_CODE
|
||||||
|
COUNTRY_CODE=${COUNTRY_CODE:-US}
|
||||||
|
|
||||||
|
# Generate wpa_supplicant.conf
|
||||||
|
cat > wpa_supplicant.conf << WIFIEOF
|
||||||
|
# WiFi Configuration for Info-Beamer Device
|
||||||
|
country=${COUNTRY_CODE}
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
update_config=1
|
||||||
|
|
||||||
|
# Primary WiFi Network
|
||||||
|
network={
|
||||||
|
ssid="${WIFI_SSID}"
|
||||||
|
psk="${WIFI_PASSWORD}"
|
||||||
|
priority=1
|
||||||
|
scan_ssid=1
|
||||||
|
}
|
||||||
|
WIFIEOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ WiFi configuration saved to wpa_supplicant.conf"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Deployment Instructions:"
|
||||||
|
echo "1. Copy wpa_supplicant.conf to SD card at: /boot/wpa_supplicant.conf"
|
||||||
|
echo "2. Copy other package files (node.lua, config.json, roboto.ttf) to SD card root"
|
||||||
|
echo "3. Insert SD card and power on device"
|
||||||
|
echo "4. Device will connect to WiFi: ${WIFI_SSID}"
|
||||||
|
echo ""
|
||||||
29
deployment-package/network-config.txt
Normal file
29
deployment-package/network-config.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Network Configuration Guide for Info-Beamer Device
|
||||||
|
|
||||||
|
## WiFi Configuration
|
||||||
|
1. Edit wpa_supplicant.conf with your WiFi credentials
|
||||||
|
2. Copy to SD card at: /boot/wpa_supplicant.conf
|
||||||
|
3. Device will auto-connect on boot
|
||||||
|
|
||||||
|
## Static IP Configuration (Optional)
|
||||||
|
If you want a fixed IP instead of DHCP, create this file on SD card:
|
||||||
|
|
||||||
|
File: /boot/dhcpcd.conf
|
||||||
|
Content:
|
||||||
|
interface wlan0
|
||||||
|
static ip_address=192.168.1.100/24
|
||||||
|
static routers=192.168.1.1
|
||||||
|
static domain_name_servers=192.168.1.1 8.8.8.8
|
||||||
|
|
||||||
|
## Ethernet Configuration
|
||||||
|
For wired connection, device will use DHCP by default.
|
||||||
|
No configuration needed if using ethernet cable.
|
||||||
|
|
||||||
|
## WiFi Country Codes
|
||||||
|
US - United States
|
||||||
|
GB - United Kingdom
|
||||||
|
DE - Germany
|
||||||
|
FR - France
|
||||||
|
CA - Canada
|
||||||
|
AU - Australia
|
||||||
|
JP - Japan
|
||||||
207
deployment-package/node.lua
Normal file
207
deployment-package/node.lua
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
-- Info-Beamer script with streaming channel support
|
||||||
|
gl.setup(NATIVE_WIDTH, NATIVE_HEIGHT)
|
||||||
|
|
||||||
|
local json = require "json"
|
||||||
|
local font = resource.load_font "roboto.ttf"
|
||||||
|
|
||||||
|
local server_url = CONFIG.server_url or "http://192.168.1.22"
|
||||||
|
local player_id = CONFIG.player_id or "default_player"
|
||||||
|
local refresh_interval = CONFIG.refresh_interval or 30
|
||||||
|
local default_duration = CONFIG.default_duration or 10
|
||||||
|
local touch_enabled = CONFIG.touch_enabled ~= false -- Default to true
|
||||||
|
local swipe_threshold = CONFIG.swipe_threshold or 100
|
||||||
|
local manual_override_duration = CONFIG.manual_override_duration or 10
|
||||||
|
|
||||||
|
local playlist = {}
|
||||||
|
local current_item = 1
|
||||||
|
local item_start_time = 0
|
||||||
|
local last_update = 0
|
||||||
|
local channel_info = {}
|
||||||
|
|
||||||
|
-- Touch and swipe variables
|
||||||
|
local touch_start_x = 0
|
||||||
|
local touch_start_y = 0
|
||||||
|
local touch_start_time = 0
|
||||||
|
local is_touching = false
|
||||||
|
local swipe_time_limit = 1.0 -- Maximum time for swipe (seconds)
|
||||||
|
local manual_override = false
|
||||||
|
local manual_override_time = 0
|
||||||
|
|
||||||
|
function update_playlist()
|
||||||
|
-- Fetch channel content from server
|
||||||
|
local url = server_url .. "/api/player/" .. player_id .. "/content"
|
||||||
|
util.post_and_wait(url, "", function(response)
|
||||||
|
if response.success then
|
||||||
|
local data = json.decode(response.content)
|
||||||
|
if data and data.content then
|
||||||
|
playlist = data.content
|
||||||
|
print("Updated channel content: " .. #playlist .. " items")
|
||||||
|
|
||||||
|
-- Load assets
|
||||||
|
for i, item in ipairs(playlist) do
|
||||||
|
if item.type == "image" then
|
||||||
|
item.resource = resource.load_image(server_url .. "/uploads/" .. item.filename)
|
||||||
|
elseif item.type == "video" then
|
||||||
|
item.resource = resource.load_video(server_url .. "/uploads/" .. item.filename)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Also fetch channel information
|
||||||
|
local channel_url = server_url .. "/api/player/" .. player_id .. "/channel"
|
||||||
|
util.post_and_wait(channel_url, "", function(response)
|
||||||
|
if response.success then
|
||||||
|
channel_info = json.decode(response.content) or {}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
last_update = sys.now()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Touch event handlers
|
||||||
|
function node.on_touch(touches)
|
||||||
|
for touch in touches() do
|
||||||
|
if touch.type == "start" then
|
||||||
|
-- Touch started
|
||||||
|
touch_start_x = touch.x
|
||||||
|
touch_start_y = touch.y
|
||||||
|
touch_start_time = sys.now()
|
||||||
|
is_touching = true
|
||||||
|
|
||||||
|
elseif touch.type == "end" and is_touching then
|
||||||
|
-- Touch ended - check for swipe
|
||||||
|
local touch_end_x = touch.x
|
||||||
|
local touch_end_y = touch.y
|
||||||
|
local touch_duration = sys.now() - touch_start_time
|
||||||
|
|
||||||
|
-- Calculate swipe distance and direction
|
||||||
|
local dx = touch_end_x - touch_start_x
|
||||||
|
local dy = touch_end_y - touch_start_y
|
||||||
|
local distance = math.sqrt(dx * dx + dy * dy)
|
||||||
|
|
||||||
|
-- Check if it's a valid swipe (horizontal, sufficient distance, quick enough)
|
||||||
|
if distance > swipe_threshold and
|
||||||
|
touch_duration < swipe_time_limit and
|
||||||
|
math.abs(dx) > math.abs(dy) * 2 then -- More horizontal than vertical
|
||||||
|
|
||||||
|
if #playlist > 1 then -- Only allow swipe if there's content to switch to
|
||||||
|
if dx > 0 then
|
||||||
|
-- Swipe right - next item
|
||||||
|
next_content()
|
||||||
|
else
|
||||||
|
-- Swipe left - previous item
|
||||||
|
previous_content()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Enable manual override mode
|
||||||
|
manual_override = true
|
||||||
|
manual_override_time = sys.now()
|
||||||
|
|
||||||
|
print("Swipe detected: " .. (dx > 0 and "right (next)" or "left (previous)"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
is_touching = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Function to go to next content item
|
||||||
|
function next_content()
|
||||||
|
if #playlist > 0 then
|
||||||
|
current_item = current_item + 1
|
||||||
|
if current_item > #playlist then
|
||||||
|
current_item = 1
|
||||||
|
end
|
||||||
|
item_start_time = sys.now()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Function to go to previous content item
|
||||||
|
function previous_content()
|
||||||
|
if #playlist > 0 then
|
||||||
|
current_item = current_item - 1
|
||||||
|
if current_item < 1 then
|
||||||
|
current_item = #playlist
|
||||||
|
end
|
||||||
|
item_start_time = sys.now()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function node.render()
|
||||||
|
-- Update playlist periodically
|
||||||
|
if sys.now() - last_update > refresh_interval then
|
||||||
|
update_playlist()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if manual override has expired
|
||||||
|
if manual_override and sys.now() - manual_override_time > manual_override_duration then
|
||||||
|
manual_override = false
|
||||||
|
item_start_time = sys.now() -- Reset automatic timing
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If no playlist items, show waiting message
|
||||||
|
if #playlist == 0 then
|
||||||
|
font:write(100, 100, "Waiting for channel content...", 50, 1, 1, 1, 1)
|
||||||
|
font:write(100, 200, "Server: " .. server_url, 30, 0.7, 0.7, 0.7, 1)
|
||||||
|
font:write(100, 250, "Player ID: " .. player_id, 30, 0.7, 0.7, 0.7, 1)
|
||||||
|
font:write(100, 300, "Swipe left/right to navigate when content is available", 20, 0.5, 0.5, 0.5, 1)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local item = playlist[current_item]
|
||||||
|
if not item then
|
||||||
|
current_item = 1
|
||||||
|
item = playlist[current_item]
|
||||||
|
if not item then return end
|
||||||
|
end
|
||||||
|
|
||||||
|
local duration = item.duration or default_duration
|
||||||
|
|
||||||
|
-- Only advance automatically if not in manual override mode
|
||||||
|
if not manual_override and sys.now() - item_start_time > duration then
|
||||||
|
next_content()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Display current item
|
||||||
|
if item and item.resource then
|
||||||
|
if item.type == "image" then
|
||||||
|
item.resource:draw(0, 0, WIDTH, HEIGHT)
|
||||||
|
elseif item.type == "video" then
|
||||||
|
item.resource:draw(0, 0, WIDTH, HEIGHT)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show channel info overlay
|
||||||
|
if channel_info.name then
|
||||||
|
font:write(10, HEIGHT - 80, "Channel: " .. channel_info.name, 20, 1, 1, 1, 0.8)
|
||||||
|
if channel_info.description then
|
||||||
|
font:write(10, HEIGHT - 50, channel_info.description, 18, 1, 1, 1, 0.7)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show content navigation info
|
||||||
|
if #playlist > 1 then
|
||||||
|
local nav_text = string.format("%d / %d", current_item, #playlist)
|
||||||
|
font:write(10, HEIGHT - 20, nav_text, 16, 1, 1, 1, 0.6)
|
||||||
|
|
||||||
|
-- Show swipe instructions (fade in/out)
|
||||||
|
local instructions_alpha = 0.4 + 0.2 * math.sin(sys.now() * 2)
|
||||||
|
font:write(10, 30, "◀ Swipe to navigate ▶", 18, 1, 1, 1, instructions_alpha)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show manual override indicator
|
||||||
|
if manual_override then
|
||||||
|
local remaining = manual_override_duration - (sys.now() - manual_override_time)
|
||||||
|
font:write(WIDTH - 150, HEIGHT - 20, string.format("Manual: %.1fs", remaining), 16, 1, 1, 0, 0.8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show current time
|
||||||
|
local current_time = os.date("%H:%M")
|
||||||
|
font:write(WIDTH - 100, 20, current_time, 24, 1, 1, 1, 0.9)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initial load
|
||||||
|
update_playlist()
|
||||||
BIN
deployment-package/roboto.ttf
Normal file
BIN
deployment-package/roboto.ttf
Normal file
Binary file not shown.
12
deployment-package/wpa_supplicant.conf
Normal file
12
deployment-package/wpa_supplicant.conf
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# WiFi Configuration for Info-Beamer Device
|
||||||
|
country=US
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
update_config=1
|
||||||
|
|
||||||
|
# Primary WiFi Network
|
||||||
|
network={
|
||||||
|
ssid="HarBAck2.4"
|
||||||
|
psk="EuropaUnita2020"
|
||||||
|
priority=1
|
||||||
|
scan_ssid=1
|
||||||
|
}
|
||||||
16
device-info-template.txt
Normal file
16
device-info-template.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
=== Info-Beamer Device Information Needed ===
|
||||||
|
|
||||||
|
1. Device ID: _________________ (from player screen)
|
||||||
|
2. Device Name: _______________ (what you named it)
|
||||||
|
3. Resolution: ________________ (from dashboard)
|
||||||
|
4. Status: ___________________ (should be 'online')
|
||||||
|
|
||||||
|
=== Next Steps ===
|
||||||
|
|
||||||
|
1. Record your actual Device ID above
|
||||||
|
2. Update our deployment package if different from '12131415'
|
||||||
|
3. Test our Flask server connection
|
||||||
|
4. Deploy our touch-enabled package
|
||||||
|
|
||||||
|
|
||||||
36
info-beamer-wifi-setup.txt
Normal file
36
info-beamer-wifi-setup.txt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Info-Beamer OS WiFi Configuration
|
||||||
|
|
||||||
|
## Quick Setup (Recommended):
|
||||||
|
|
||||||
|
1. After flashing SD card with Info-Beamer OS:
|
||||||
|
- Don't eject SD card yet
|
||||||
|
- Open the SD card in file explorer
|
||||||
|
- Create new file: wpa_supplicant.conf
|
||||||
|
- Add this content:
|
||||||
|
|
||||||
|
country=US
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
update_config=1
|
||||||
|
|
||||||
|
network={
|
||||||
|
ssid="YOUR_WIFI_NAME"
|
||||||
|
psk="YOUR_WIFI_PASSWORD"
|
||||||
|
key_mgmt=WPA-PSK
|
||||||
|
}
|
||||||
|
|
||||||
|
2. Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD
|
||||||
|
3. Save file and eject SD card
|
||||||
|
4. Insert in Pi and boot
|
||||||
|
|
||||||
|
## Alternative: Pi Imager Method
|
||||||
|
- Use gear icon in Pi Imager
|
||||||
|
- Enable "Configure wireless LAN"
|
||||||
|
- Enter WiFi credentials before flashing
|
||||||
|
|
||||||
|
## Country Codes:
|
||||||
|
US, GB, DE, FR, IT, ES, CA, AU, etc.
|
||||||
|
|
||||||
|
## Important:
|
||||||
|
- Country code is REQUIRED
|
||||||
|
- File goes in /boot partition
|
||||||
|
- Use quotes around SSID/password
|
||||||
33
infobeamer-setup-checklist.txt
Normal file
33
infobeamer-setup-checklist.txt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Info-Beamer Player Setup Checklist
|
||||||
|
|
||||||
|
## 1. Connect Device to Account:
|
||||||
|
- Go to: https://info-beamer.com/
|
||||||
|
- Create account or login
|
||||||
|
- Click "Add Device"
|
||||||
|
- Enter the ID shown on your player screen
|
||||||
|
- Device should appear in your dashboard
|
||||||
|
|
||||||
|
## 2. Verify Device Status:
|
||||||
|
- Device shows "online" status in dashboard
|
||||||
|
- Last seen timestamp is recent
|
||||||
|
- Device info shows correct hardware details
|
||||||
|
|
||||||
|
## 3. Test Basic Functionality:
|
||||||
|
- Upload a simple image to test
|
||||||
|
- Assign to your device
|
||||||
|
- Image should display on screen within 30 seconds
|
||||||
|
|
||||||
|
## 4. Configure for Our Flask Server:
|
||||||
|
- Device ID needed: Record the ID from screen
|
||||||
|
- Update our server.py with correct device ID
|
||||||
|
- Test API endpoints work
|
||||||
|
|
||||||
|
## 5. Deploy Our Package:
|
||||||
|
- Upload our node.lua package to Info-Beamer
|
||||||
|
- Assign package to device
|
||||||
|
- Device should connect to Flask server at 192.168.1.22
|
||||||
|
|
||||||
|
## Troubleshooting:
|
||||||
|
- If device offline: Check WiFi, restart device
|
||||||
|
- If package not loading: Check node.lua syntax
|
||||||
|
- If server connection fails: Check Flask server running on port 80
|
||||||
122
node.lua
122
node.lua
@@ -8,6 +8,9 @@ local server_url = CONFIG.server_url or "http://192.168.1.22"
|
|||||||
local player_id = CONFIG.player_id or "default_player"
|
local player_id = CONFIG.player_id or "default_player"
|
||||||
local refresh_interval = CONFIG.refresh_interval or 30
|
local refresh_interval = CONFIG.refresh_interval or 30
|
||||||
local default_duration = CONFIG.default_duration or 10
|
local default_duration = CONFIG.default_duration or 10
|
||||||
|
local touch_enabled = CONFIG.touch_enabled ~= false -- Default to true
|
||||||
|
local swipe_threshold = CONFIG.swipe_threshold or 100
|
||||||
|
local manual_override_duration = CONFIG.manual_override_duration or 10
|
||||||
|
|
||||||
local playlist = {}
|
local playlist = {}
|
||||||
local current_item = 1
|
local current_item = 1
|
||||||
@@ -15,6 +18,15 @@ local item_start_time = 0
|
|||||||
local last_update = 0
|
local last_update = 0
|
||||||
local channel_info = {}
|
local channel_info = {}
|
||||||
|
|
||||||
|
-- Touch and swipe variables
|
||||||
|
local touch_start_x = 0
|
||||||
|
local touch_start_y = 0
|
||||||
|
local touch_start_time = 0
|
||||||
|
local is_touching = false
|
||||||
|
local swipe_time_limit = 1.0 -- Maximum time for swipe (seconds)
|
||||||
|
local manual_override = false
|
||||||
|
local manual_override_time = 0
|
||||||
|
|
||||||
function update_playlist()
|
function update_playlist()
|
||||||
-- Fetch channel content from server
|
-- Fetch channel content from server
|
||||||
local url = server_url .. "/api/player/" .. player_id .. "/content"
|
local url = server_url .. "/api/player/" .. player_id .. "/content"
|
||||||
@@ -48,17 +60,94 @@ function update_playlist()
|
|||||||
last_update = sys.now()
|
last_update = sys.now()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Touch event handlers
|
||||||
|
function node.on_touch(touches)
|
||||||
|
for touch in touches() do
|
||||||
|
if touch.type == "start" then
|
||||||
|
-- Touch started
|
||||||
|
touch_start_x = touch.x
|
||||||
|
touch_start_y = touch.y
|
||||||
|
touch_start_time = sys.now()
|
||||||
|
is_touching = true
|
||||||
|
|
||||||
|
elseif touch.type == "end" and is_touching then
|
||||||
|
-- Touch ended - check for swipe
|
||||||
|
local touch_end_x = touch.x
|
||||||
|
local touch_end_y = touch.y
|
||||||
|
local touch_duration = sys.now() - touch_start_time
|
||||||
|
|
||||||
|
-- Calculate swipe distance and direction
|
||||||
|
local dx = touch_end_x - touch_start_x
|
||||||
|
local dy = touch_end_y - touch_start_y
|
||||||
|
local distance = math.sqrt(dx * dx + dy * dy)
|
||||||
|
|
||||||
|
-- Check if it's a valid swipe (horizontal, sufficient distance, quick enough)
|
||||||
|
if distance > swipe_threshold and
|
||||||
|
touch_duration < swipe_time_limit and
|
||||||
|
math.abs(dx) > math.abs(dy) * 2 then -- More horizontal than vertical
|
||||||
|
|
||||||
|
if #playlist > 1 then -- Only allow swipe if there's content to switch to
|
||||||
|
if dx > 0 then
|
||||||
|
-- Swipe right - next item
|
||||||
|
next_content()
|
||||||
|
else
|
||||||
|
-- Swipe left - previous item
|
||||||
|
previous_content()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Enable manual override mode
|
||||||
|
manual_override = true
|
||||||
|
manual_override_time = sys.now()
|
||||||
|
|
||||||
|
print("Swipe detected: " .. (dx > 0 and "right (next)" or "left (previous)"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
is_touching = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Function to go to next content item
|
||||||
|
function next_content()
|
||||||
|
if #playlist > 0 then
|
||||||
|
current_item = current_item + 1
|
||||||
|
if current_item > #playlist then
|
||||||
|
current_item = 1
|
||||||
|
end
|
||||||
|
item_start_time = sys.now()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Function to go to previous content item
|
||||||
|
function previous_content()
|
||||||
|
if #playlist > 0 then
|
||||||
|
current_item = current_item - 1
|
||||||
|
if current_item < 1 then
|
||||||
|
current_item = #playlist
|
||||||
|
end
|
||||||
|
item_start_time = sys.now()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function node.render()
|
function node.render()
|
||||||
-- Update playlist periodically
|
-- Update playlist periodically
|
||||||
if sys.now() - last_update > refresh_interval then
|
if sys.now() - last_update > refresh_interval then
|
||||||
update_playlist()
|
update_playlist()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Check if manual override has expired
|
||||||
|
if manual_override and sys.now() - manual_override_time > manual_override_duration then
|
||||||
|
manual_override = false
|
||||||
|
item_start_time = sys.now() -- Reset automatic timing
|
||||||
|
end
|
||||||
|
|
||||||
-- If no playlist items, show waiting message
|
-- If no playlist items, show waiting message
|
||||||
if #playlist == 0 then
|
if #playlist == 0 then
|
||||||
font:write(100, 100, "Waiting for channel content...", 50, 1, 1, 1, 1)
|
font:write(100, 100, "Waiting for channel content...", 50, 1, 1, 1, 1)
|
||||||
font:write(100, 200, "Server: " .. server_url, 30, 0.7, 0.7, 0.7, 1)
|
font:write(100, 200, "Server: " .. server_url, 30, 0.7, 0.7, 0.7, 1)
|
||||||
font:write(100, 250, "Player ID: " .. player_id, 30, 0.7, 0.7, 0.7, 1)
|
font:write(100, 250, "Player ID: " .. player_id, 30, 0.7, 0.7, 0.7, 1)
|
||||||
|
font:write(100, 300, "Swipe left/right to navigate when content is available", 20, 0.5, 0.5, 0.5, 1)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -71,14 +160,9 @@ function node.render()
|
|||||||
|
|
||||||
local duration = item.duration or default_duration
|
local duration = item.duration or default_duration
|
||||||
|
|
||||||
-- Check if it's time to move to next item
|
-- Only advance automatically if not in manual override mode
|
||||||
if sys.now() - item_start_time > duration then
|
if not manual_override and sys.now() - item_start_time > duration then
|
||||||
current_item = current_item + 1
|
next_content()
|
||||||
if current_item > #playlist then
|
|
||||||
current_item = 1
|
|
||||||
end
|
|
||||||
item_start_time = sys.now()
|
|
||||||
item = playlist[current_item]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Display current item
|
-- Display current item
|
||||||
@@ -89,13 +173,29 @@ function node.render()
|
|||||||
item.resource:draw(0, 0, WIDTH, HEIGHT)
|
item.resource:draw(0, 0, WIDTH, HEIGHT)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Show channel info overlay (optional)
|
-- Show channel info overlay
|
||||||
if channel_info.name then
|
if channel_info.name then
|
||||||
font:write(10, HEIGHT - 60, "Channel: " .. channel_info.name, 20, 1, 1, 1, 0.8)
|
font:write(10, HEIGHT - 80, "Channel: " .. channel_info.name, 20, 1, 1, 1, 0.8)
|
||||||
if channel_info.description then
|
if channel_info.description then
|
||||||
font:write(10, HEIGHT - 30, channel_info.description, 20, 1, 1, 1, 0.8)
|
font:write(10, HEIGHT - 50, channel_info.description, 18, 1, 1, 1, 0.7)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Show content navigation info
|
||||||
|
if #playlist > 1 then
|
||||||
|
local nav_text = string.format("%d / %d", current_item, #playlist)
|
||||||
|
font:write(10, HEIGHT - 20, nav_text, 16, 1, 1, 1, 0.6)
|
||||||
|
|
||||||
|
-- Show swipe instructions (fade in/out)
|
||||||
|
local instructions_alpha = 0.4 + 0.2 * math.sin(sys.now() * 2)
|
||||||
|
font:write(10, 30, "◀ Swipe to navigate ▶", 18, 1, 1, 1, instructions_alpha)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show manual override indicator
|
||||||
|
if manual_override then
|
||||||
|
local remaining = manual_override_duration - (sys.now() - manual_override_time)
|
||||||
|
font:write(WIDTH - 150, HEIGHT - 20, string.format("Manual: %.1fs", remaining), 16, 1, 1, 0, 0.8)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Show current time
|
-- Show current time
|
||||||
|
|||||||
178
server.py
178
server.py
@@ -199,13 +199,19 @@ def admin():
|
|||||||
@app.route('/user')
|
@app.route('/user')
|
||||||
@login_required
|
@login_required
|
||||||
def user_dashboard():
|
def user_dashboard():
|
||||||
# Get user's files
|
# Get all active files (for signage systems, users should see all available media)
|
||||||
user_files = MediaFile.query.filter_by(uploaded_by=current_user.id, is_active=True).order_by(MediaFile.upload_date.desc()).all()
|
user_files = MediaFile.query.filter_by(is_active=True).order_by(MediaFile.upload_date.desc()).all()
|
||||||
|
|
||||||
|
# Get all active channels
|
||||||
|
channels = StreamingChannel.query.filter_by(is_active=True).order_by(StreamingChannel.name).all()
|
||||||
|
|
||||||
|
# Get all users for display in file info
|
||||||
|
users = User.query.all()
|
||||||
|
|
||||||
# Get user's activity
|
# Get user's activity
|
||||||
user_activity = ActivityLog.query.filter_by(user_id=current_user.id).order_by(ActivityLog.timestamp.desc()).limit(10).all()
|
user_activity = ActivityLog.query.filter_by(user_id=current_user.id).order_by(ActivityLog.timestamp.desc()).limit(10).all()
|
||||||
|
|
||||||
return render_template('user.html', user_files=user_files, user_activity=user_activity)
|
return render_template('user.html', user_files=user_files, user_activity=user_activity, channels=channels, users=users)
|
||||||
|
|
||||||
# File Management Routes
|
# File Management Routes
|
||||||
@app.route('/upload', methods=['POST'])
|
@app.route('/upload', methods=['POST'])
|
||||||
@@ -243,8 +249,39 @@ def upload_file():
|
|||||||
db.session.add(media_file)
|
db.session.add(media_file)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
log_activity('File uploaded', f'Uploaded: {file.filename}')
|
# If channel is selected, add to channel
|
||||||
|
channel_id = request.form.get('channel_id')
|
||||||
|
display_duration = request.form.get('display_duration', 10)
|
||||||
|
|
||||||
|
if channel_id and channel_id.strip():
|
||||||
|
try:
|
||||||
|
channel_id = int(channel_id)
|
||||||
|
channel = StreamingChannel.query.get(channel_id)
|
||||||
|
|
||||||
|
if channel and channel.is_active:
|
||||||
|
# Get next order number for this channel
|
||||||
|
max_order = db.session.query(db.func.max(ChannelContent.order_index)).filter_by(channel_id=channel_id).scalar() or 0
|
||||||
|
|
||||||
|
# Add to channel
|
||||||
|
channel_content = ChannelContent(
|
||||||
|
channel_id=channel_id,
|
||||||
|
media_file_id=media_file.id,
|
||||||
|
display_duration=int(display_duration),
|
||||||
|
order_index=max_order + 1
|
||||||
|
)
|
||||||
|
db.session.add(channel_content)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_activity('Content added to channel', f'Added {file.filename} to channel {channel.name}')
|
||||||
|
flash(f'File "{file.filename}" uploaded and added to channel "{channel.name}"!', 'success')
|
||||||
|
else:
|
||||||
|
flash(f'File "{file.filename}" uploaded but channel not found!', 'warning')
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
flash(f'File "{file.filename}" uploaded but invalid channel selected!', 'warning')
|
||||||
|
else:
|
||||||
flash(f'File "{file.filename}" uploaded successfully!', 'success')
|
flash(f'File "{file.filename}" uploaded successfully!', 'success')
|
||||||
|
|
||||||
|
log_activity('File uploaded', f'Uploaded: {file.filename}')
|
||||||
else:
|
else:
|
||||||
flash('Invalid file type. Please upload images, videos, PDFs, or JSON files.', 'error')
|
flash('Invalid file type. Please upload images, videos, PDFs, or JSON files.', 'error')
|
||||||
|
|
||||||
@@ -316,6 +353,139 @@ def add_channel():
|
|||||||
|
|
||||||
return redirect(url_for('view_channels'))
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
@app.route('/delete_channel/<int:channel_id>')
|
||||||
|
@login_required
|
||||||
|
def delete_channel(channel_id):
|
||||||
|
channel = StreamingChannel.query.get_or_404(channel_id)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if current_user.role != 'admin' and channel.created_by != current_user.id:
|
||||||
|
flash('Access denied. You can only delete your own channels.', 'error')
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
# Prevent deleting default channel
|
||||||
|
if channel.is_default:
|
||||||
|
flash('Cannot delete the default channel.', 'error')
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
# Check if any players are assigned to this channel
|
||||||
|
assigned_players = Player.query.filter_by(channel_id=channel_id).count()
|
||||||
|
if assigned_players > 0:
|
||||||
|
flash(f'Cannot delete channel. {assigned_players} player(s) are assigned to this channel.', 'error')
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
channel_name = channel.name
|
||||||
|
db.session.delete(channel)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_activity('Channel deleted', f'Deleted channel: {channel_name}')
|
||||||
|
flash(f'Channel "{channel_name}" deleted successfully!', 'success')
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
@app.route('/edit_channel/<int:channel_id>', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def edit_channel(channel_id):
|
||||||
|
channel = StreamingChannel.query.get_or_404(channel_id)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if current_user.role != 'admin' and channel.created_by != current_user.id:
|
||||||
|
flash('Access denied. You can only edit your own channels.', 'error')
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
# If setting as default, unset other defaults
|
||||||
|
is_default = 'is_default' in request.form
|
||||||
|
if is_default and not channel.is_default:
|
||||||
|
StreamingChannel.query.filter_by(is_default=True).update({'is_default': False})
|
||||||
|
|
||||||
|
channel.name = request.form['name']
|
||||||
|
channel.description = request.form.get('description', '')
|
||||||
|
channel.is_default = is_default
|
||||||
|
channel.is_active = 'is_active' in request.form
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_activity('Channel updated', f'Updated channel: {channel.name}')
|
||||||
|
flash(f'Channel "{channel.name}" updated successfully!', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
flash(f'Error updating channel: {str(e)}', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
return render_template('edit_channel.html', channel=channel)
|
||||||
|
|
||||||
|
@app.route('/manage_content/<int:channel_id>')
|
||||||
|
@login_required
|
||||||
|
def manage_content(channel_id):
|
||||||
|
channel = StreamingChannel.query.get_or_404(channel_id)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if current_user.role != 'admin' and channel.created_by != current_user.id:
|
||||||
|
flash('Access denied. You can only manage content of your own channels.', 'error')
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
# Get all available media files
|
||||||
|
available_media = MediaFile.query.filter_by(is_active=True).order_by(MediaFile.upload_date.desc()).all()
|
||||||
|
|
||||||
|
# Get current channel content with media file details
|
||||||
|
channel_content = ChannelContent.query.filter_by(channel_id=channel_id).order_by(ChannelContent.order_index).all()
|
||||||
|
|
||||||
|
return render_template('manage_content.html', channel=channel, available_media=available_media, channel_content=channel_content)
|
||||||
|
|
||||||
|
@app.route('/add_content_to_channel', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def add_content_to_channel():
|
||||||
|
channel_id = request.form['channel_id']
|
||||||
|
media_file_id = request.form['media_file_id']
|
||||||
|
duration = request.form.get('duration', 10)
|
||||||
|
|
||||||
|
channel = StreamingChannel.query.get_or_404(channel_id)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if current_user.role != 'admin' and channel.created_by != current_user.id:
|
||||||
|
flash('Access denied.', 'error')
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
# Get next order number
|
||||||
|
max_order = db.session.query(db.func.max(ChannelContent.order_index)).filter_by(channel_id=channel_id).scalar() or 0
|
||||||
|
|
||||||
|
# Add content
|
||||||
|
content = ChannelContent(
|
||||||
|
channel_id=channel_id,
|
||||||
|
media_file_id=media_file_id,
|
||||||
|
display_duration=int(duration),
|
||||||
|
order_index=max_order + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(content)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
media_file = MediaFile.query.get(media_file_id)
|
||||||
|
log_activity('Content added to channel', f'Added {media_file.original_name} to {channel.name}')
|
||||||
|
flash('Content added to channel successfully!', 'success')
|
||||||
|
|
||||||
|
return redirect(url_for('manage_content', channel_id=channel_id))
|
||||||
|
|
||||||
|
@app.route('/remove_content_from_channel/<int:content_id>')
|
||||||
|
@login_required
|
||||||
|
def remove_content_from_channel(content_id):
|
||||||
|
content = ChannelContent.query.get_or_404(content_id)
|
||||||
|
channel = content.channel
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if current_user.role != 'admin' and channel.created_by != current_user.id:
|
||||||
|
flash('Access denied.', 'error')
|
||||||
|
return redirect(url_for('view_channels'))
|
||||||
|
|
||||||
|
db.session.delete(content)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_activity('Content removed from channel', f'Removed content from {channel.name}')
|
||||||
|
flash('Content removed from channel successfully!', 'success')
|
||||||
|
|
||||||
|
return redirect(url_for('manage_content', channel_id=channel.id))
|
||||||
|
|
||||||
# User Management Routes
|
# User Management Routes
|
||||||
@app.route('/add_user', methods=['POST'])
|
@app.route('/add_user', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -57,7 +57,11 @@
|
|||||||
<i class="bi bi-person-circle"></i> {{ current_user.username }}
|
<i class="bi bi-person-circle"></i> {{ current_user.username }}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin') }}"><i class="bi bi-house"></i> Dashboard</a></li>
|
{% if current_user.role == 'admin' %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('admin') }}"><i class="bi bi-house"></i> Admin Dashboard</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('user_dashboard') }}"><i class="bi bi-house"></i> User Dashboard</a></li>
|
||||||
|
{% endif %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('view_schedules') }}"><i class="bi bi-calendar"></i> Schedules</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('view_schedules') }}"><i class="bi bi-calendar"></i> Schedules</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('view_channels') }}"><i class="bi bi-collection-play"></i> Channels</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('view_channels') }}"><i class="bi bi-collection-play"></i> Channels</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
@@ -259,16 +263,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editChannel(channelId) {
|
function editChannel(channelId) {
|
||||||
alert('Edit channel functionality coming soon!');
|
window.location.href = `/edit_channel/${channelId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function manageContent(channelId) {
|
function manageContent(channelId) {
|
||||||
alert('Content management functionality coming soon!');
|
window.location.href = `/manage_content/${channelId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteChannel(channelId) {
|
function deleteChannel(channelId) {
|
||||||
if (confirm('Are you sure you want to delete this channel?')) {
|
if (confirm('Are you sure you want to delete this channel? This action cannot be undone.')) {
|
||||||
alert('Delete channel functionality coming soon!');
|
window.location.href = `/delete_channel/${channelId}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
templates/edit_channel.html
Normal file
39
templates/edit_channel.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Edit Channel</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2>Edit Channel</h2>
|
||||||
|
<form method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Channel Name</label>
|
||||||
|
<input type="text" class="form-control" name="name" value="{{ channel.name }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Description</label>
|
||||||
|
<textarea class="form-control" name="description">{{ channel.description or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_default" {{ 'checked' if channel.is_default else '' }}>
|
||||||
|
<label class="form-check-label">Set as default channel</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_active" {{ 'checked' if channel.is_active else '' }}>
|
||||||
|
<label class="form-check-label">Channel is active</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Update Channel</button>
|
||||||
|
<a href="{{ url_for('view_channels') }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
templates/manage_content.html
Normal file
13
templates/manage_content.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Manage Content</title><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"></head>
|
||||||
|
<body><div class="container mt-4"><h2>{{ channel.name }} - Content Management</h2>
|
||||||
|
<div class="row"><div class="col-md-6"><h4>Current Content</h4>
|
||||||
|
{% for content in channel_content %}<div class="card mb-2"><div class="card-body">{{ content.media_file.original_name }} ({{ content.display_duration }}s)
|
||||||
|
<a href="/remove_content_from_channel/{{ content.id }}" class="btn btn-sm btn-danger float-end">Remove</a></div></div>{% endfor %}
|
||||||
|
</div><div class="col-md-6"><h4>Add Content</h4><form method="post" action="/add_content_to_channel">
|
||||||
|
<input type="hidden" name="channel_id" value="{{ channel.id }}"><select name="media_file_id" class="form-select mb-2">
|
||||||
|
{% for media in available_media %}<option value="{{ media.id }}">{{ media.original_name }}</option>{% endfor %}</select>
|
||||||
|
<input type="number" name="duration" value="10" class="form-control mb-2" placeholder="Duration">
|
||||||
|
<button type="submit" class="btn btn-success">Add</button></form></div></div>
|
||||||
|
<a href="/channels" class="btn btn-primary mt-3">Back</a></div></body></html>
|
||||||
@@ -74,19 +74,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" enctype="multipart/form-data" action="/upload" class="row g-3">
|
<form method="post" enctype="multipart/form-data" action="/upload" class="row g-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-12">
|
||||||
<div class="input-group">
|
<label for="file" class="form-label">Select Media File</label>
|
||||||
<input type="file" class="form-control" name="file" accept=".png,.jpg,.jpeg,.gif,.mp4,.avi,.mov,.pdf" required>
|
<input type="file" class="form-control" name="file" id="file" accept=".png,.jpg,.jpeg,.gif,.mp4,.avi,.mov,.pdf" required>
|
||||||
|
<small class="text-muted">Supported: Images, Videos, PDFs</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="channel_id" class="form-label">Add to Channel (Optional)</label>
|
||||||
|
<select class="form-select" name="channel_id" id="channel_id">
|
||||||
|
<option value="">Select Channel...</option>
|
||||||
|
{% for channel in channels %}
|
||||||
|
<option value="{{ channel.id }}">{{ channel.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="display_duration" class="form-label">Display Duration (seconds)</label>
|
||||||
|
<input type="number" class="form-control" name="display_duration" id="display_duration" min="1" max="300" value="10" placeholder="10">
|
||||||
|
<small class="text-muted">How long to display this media (1-300 seconds)</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
<i class="bi bi-upload"></i> Upload
|
<i class="bi bi-upload"></i> Upload Media
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<small class="text-muted">
|
|
||||||
Supported: Images, Videos, PDFs
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,12 +110,12 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0"><i class="bi bi-file-earmark-image"></i> All Media Files</h5>
|
<h5 class="mb-0"><i class="bi bi-file-earmark-image"></i> All Media Files</h5>
|
||||||
<span class="badge bg-light text-dark">{{ files|length }} files</span>
|
<span class="badge bg-light text-dark">{{ user_files|length }} files</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if files %}
|
{% if user_files %}
|
||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% for file in files %}
|
{% for file in user_files %}
|
||||||
<div class="card media-card">
|
<div class="card media-card">
|
||||||
<div class="file-preview">
|
<div class="file-preview">
|
||||||
{% if file.file_type in ['jpg', 'jpeg', 'png', 'gif'] %}
|
{% if file.file_type in ['jpg', 'jpeg', 'png', 'gif'] %}
|
||||||
|
|||||||
BIN
tv-anunturi-deployment-complete.tar.gz
Normal file
BIN
tv-anunturi-deployment-complete.tar.gz
Normal file
Binary file not shown.
9
wpa_supplicant_template.conf
Normal file
9
wpa_supplicant_template.conf
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
country=US
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
update_config=1
|
||||||
|
|
||||||
|
network={
|
||||||
|
ssid="YOUR_WIFI_NETWORK_NAME"
|
||||||
|
psk="YOUR_WIFI_PASSWORD"
|
||||||
|
key_mgmt=WPA-PSK
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user