1 Commits
master ... dev

Author SHA1 Message Date
d9772c026d Current state of providers. 2020-02-23 12:01:57 +01:00
26 changed files with 130 additions and 1034 deletions

5
.gitignore vendored
View File

@@ -1,4 +1 @@
### Above combination will ignore all files without extension ###
bin/
.vscode/
pixctrl.js
bin/

View File

@@ -1,49 +0,0 @@
# randopix
## Usage
### Server
The server is run with an inital mode. All settings can later be changed with the client.
```
randopix
Version 0.1 - Display random images from different sources
Usage:
randopix [options]
Options:
-m, --mode=MODE The image source mode. Possible values: [none, foxes, inspiro, file]
-p, --path=PATH Path to a directory with images for the 'file' mode
-t, --timeout=TIMEOUT Seconds before the image is refreshed (default: 300)
-w, --windowed Do not start in fullscreen mode
-v, --verbose Show more information
-h, --help Show this help
```
### Client
The `pixctrl` client is used to issue commands to a running server.
Per default the client will try to connect to a server running on the same maschine. Use the `-s HOSTNAME` option to control a server over the network.
```
pixctrl
Control utilitiy for randopix
Usage:
pixctrl [options] COMMAND
Commands:
refresh Force image refresh now
timeout Set timeout in seconds before a new image is displayed
mode Change the display mode. Possible values: [none, foxes, inspiro, file]
Options:
-s, --server=SERVER Host running the randopix server (default: 127.0.0.1)
-p, --port=PORT Port to connect to the randopix server (default: 5555)
-h, --help Show this help
```
## Build
Install the [Nim Compiler](https://nim-lang.org/install.html).
Use this command to install the dependencies and build the program:
```
$ nimble build
```

View File

@@ -1,9 +0,0 @@
[Unit]
Description=randopix image display
[Service]
Environment=DISPLAY=:0
ExecStart=randopix -d /mnt/pix -m file
[Install]
WantedBy=default.target

18
randopics.nimble Normal file
View File

@@ -0,0 +1,18 @@
import strformat
# Package
version = "0.1.0"
author = "luxick"
description = "Play an image slide show from different sources"
license = "GPL-2.0"
srcDir = "src"
bin = @["randopics"]
# Dependencies
requires "nim >= 1.0.0", "gintro <= 0.5.5"
task debug, "Compile debug version":
exec "nim c --out:bin/randopics src/randopics.nim"
task release, "Compile release version":
exec fmt"nim c -d:release --out:bin/{version}/randopics src/randopics.nim"

View File

@@ -1,31 +0,0 @@
# Package
version = "1.1.0"
author = "luxick"
description = "Play an image slide show from different sources"
license = "GPL-2.0"
srcDir = "src"
binDir = "bin"
bin = @["randopix", "pixctrl"]
# Dependencies
requires "nim >= 1.0.0", "gintro", "argparse", "jester", "ajax"
# Not on nimble yet
requires "https://github.com/luxick/op.git >= 1.0.0"
proc genJS =
echo "Generating JS Client"
exec("nim js -o:src/resources/www/pixctrl.js src/pixctrl.nim")
task genJS, "Generate the Javascript client":
genJS()
task buildAll, "Generate JS and run build":
genJS()
exec "nimble build"
task debug, "Compile debug version":
exec "nim c -d:debug --debugger:native -o:bin/randopix src/randopix.nim"
before install:
genJS()

View File

@@ -1,38 +0,0 @@
import json
type
OpResult* = object of RootObj ## Result object for signalling failure state across proc calls
success*: bool ## Indicating if the opration was successfull
errorMsg*: string ## Error meassge in case the operation failed
Mode* {.pure.} = enum ## Options for the display mode
None = "none" ## No images will be displayed
Foxes = "foxes" ## Some nice foxes
Inspiro = "inspiro" ## Inspiring nonsense
File = "file" ## Images from a local path
Command* = enum
cClose = "close" ## Closes the control server and exists the applicaiton
cRefresh = "refresh" ## Force refresh of the image now
cTimeout = "timeout" ## Set image timeout to a new value
cMode = "mode" ## Change the servers display mode
CommandMessage* = object of RootObj
command*: Command ## Command that the application should execute
parameter*: string ## Optional parameter for the command
proc `$`(cMsg: CommandMessage): string =
$(%cMsg)
proc newOpResult*(): OpResult =
OpResult(success: true)
proc newOpResult*(msg: string): OpResult =
OpResult(success: false, errorMsg: msg)
proc newCommandMessage*(c: Command, p: string = ""): CommandMessage =
CommandMessage(command: c, parameter: p)
proc enumToStrings*(en: typedesc): seq[string] =
for x in en:
result.add $x

View File

@@ -1,94 +0,0 @@
import strutils, json
import common
when defined(js):
import ajax, jsconsole, dom
else:
import httpClient, strformat
import argparse
var randopixServer* {.exportc.}: string ## URL for the randopix server
proc sendCommand(msg: CommandMessage) =
when defined(js):
console.log("Sending:", $msg, "to URL:", document.URL)
var req = newXMLHttpRequest()
proc processSend(e:Event) =
if req.readyState == rsDONE:
if req.status != 200:
console.log("There was a problem with the request.")
console.log($req.status, req.statusText)
req.onreadystatechange = processSend
req.open("POST", document.URL)
req.send(cstring($(%*msg)))
else:
let client = newHttpClient()
let resp = client.post(randopixServer, $(%msg))
if not resp.status.contains("200"):
echo "Error while sending command: ", resp.status
proc sendCommand(cmd: Command) =
sendCommand(newCommandMessage(cmd))
proc switchMode*(mode: string) =
## Update the display mode
try:
discard parseEnum[Mode](mode)
except ValueError:
echo "Invalid mode: ", mode
echo "Possible values: [$1]" % Mode.enumToStrings().join(", ")
return
sendCommand(newCommandMessage(cMode, mode))
proc refresh*() {.exportc.} =
## Force refresh of the current image
sendCommand(cRefresh)
proc setTimeout*(seconds: string) =
## Set the image timeout to this value
sendCommand(newCommandMessage(cTimeout, seconds))
when defined(js):
proc getModes(): seq[cstring] {.exportc.} =
for mode in enumToStrings(Mode):
result.add cstring(mode)
proc switchMode*(mode: cstring) {.exportc.} =
switchMode($mode)
proc setTimeout*(seconds: cstring) {.exportc.} =
setTimeout($seconds)
else:
when isMainModule:
const modeHelp = "Change the display mode. Possible values: [$1]" % Mode.enumToStrings().join(", ")
var p = newParser("pixctrl"):
help("Control utilitiy for randopix")
option("-s", "--server", help="Host running the randopix server", default="http://localhost:8080/")
run:
if opts.server.startsWith("http://"):
randopixServer = opts.server
else:
randopixServer = fmt"http://{opts.server}"
command($cRefresh):
## Force refresh command
help("Force image refresh now")
run:
refresh()
command($cTimeout):
## Timeout Command
help("Set timeout in seconds before a new image is displayed")
arg("seconds", default = "300")
run:
setTimeout(opts.seconds)
command($cMode):
## Mode switch command
help(modeHelp)
arg("mode")
run:
switchMode(opts.mode)
try:
p.run(commandLineParams())
except:
echo getCurrentExceptionMsg()

View File

@@ -1,174 +0,0 @@
import os, sets, random, httpClient, json, strutils, strformat, options, deques, times
from lenientops import `*`
import op, gintro/[gdkpixbuf, gobject]
import common
const
supportedExts = @[".png", ".jpg", ".jpeg"]
placeholderImg = slurp("resources/blank.png")
foxesUrl = "https://randomfox.ca/floof/"
inspiroUrl = "http://inspirobot.me/api?generate=true"
type
ImageProvider* = ref object of RootObj
## Manages images that should be displayed
verbose: bool ## Additional logging for the image provider
mode* : Mode ## Selects the API that is used to get images
path*: Option[string] ## Path on the local file syetem that will be used in `file` mode
exts: HashSet[string] ## Allowed extensions that the `file` mode will display
var
client = newHttpClient() ## For loading images from the web
tmpDir = getTempDir() / "randopix"
tmpFile = tmpDir / "tmp.png"
fileList = initDeque[string]()
########################
# Constructors
########################
proc newImageProvider(verbose: bool, mode: Mode, path: Option[string]): ImageProvider =
ImageProvider(verbose: verbose, mode: mode, path: path, exts: supportedExts.toHashSet)
proc newImageProvider*(verbose: bool): ImageProvider =
newImageProvider(verbose, Mode.None, none(string))
proc newImageProvider*(verbose: bool, path: string): ImageProvider =
newImageProvider(verbose, Mode.None, some(path))
proc newImageProvider*(verbose: bool, mode: Mode): ImageProvider =
newImageProvider(verbose, mode, none(string))
proc newImageProvider*(verbose: bool, mode: Mode, path: string): ImageProvider =
newImageProvider(verbose, mode, some(path))
########################
# Utilities
########################
proc log(ip: ImageProvider, things: varargs[string, `$`]) =
if ip.verbose:
echo things.join()
func calcImageSize(maxWidth, maxHeight, imgWidth, imgHeight: int): tuple[width: int, height: int] =
## Calculate the best fit for an image on the give screen size.
## This should keep the image aspect ratio
let
ratioMax = maxWidth / maxHeight
ratioImg = imgWidth / imgHeight
if (ratioMax > ratioImg):
result.width = (imgWidth * (maxHeight / imgHeight)).toInt
result.height = maxHeight
else:
result.width = maxWidth
result.height = (imgHeight * (maxWidth / imgWidth)).toInt
########################
# Image Provider procs
########################
proc getPlaceHolder(ip: ImageProvider): OP[string] =
## Provide the placeholder image.
## This is used when no mode is active
let f = fmt"{tmpFile}.blank"
writeFile(f, placeholderImg)
ok f
proc getFox(ip: ImageProvider): OP[string] =
## Download image from the fox API
try:
let urlData = client.getContent(foxesUrl)
let info = parseJson(urlData)
let imageData = client.getContent(info["image"].getStr)
let dlFile = fmt"{tmpFile}.download"
writeFile(dlFile, imageData)
ok dlFile
except JsonParsingError:
ip.log fmt"Error while fetching from fox API: {getCurrentExceptionMsg()}"
fail[string] "Json parsing error"
except KeyError:
ip.log fmt"No image in downloaded data: {getCurrentExceptionMsg()}"
fail[string] "No image from API"
proc getInspiro(ip: ImageProvider): OP[string] =
## Download and save image from the inspiro API
try:
let imageUrl = client.getContent(inspiroUrl)
ip.log fmt"Downloading inspiro image from: '{imageUrl}'"
let imageData = client.getContent(imageUrl)
let dlFile = fmt"{tmpFile}.download"
writeFile(dlFile,imageData)
ok dlFile
except:
ip.log fmt"Unexpected error while downloading: {getCurrentExceptionMsg()}"
fail[string] getCurrentExceptionMsg()
proc getLocalFile(ip: var ImageProvider): OP[string] =
## Provide an image from a local folder
# First, check if there are still images left to be loaded.
# If not reread all files from the path
if fileList.len == 0:
var tmp: seq[string]
var split: tuple[dir, name, ext: string]
for file in walkDirRec(ip.path.get):
split = splitFile(file)
if ip.exts.contains(split.ext):
tmp.add($file)
ip.log fmt"Loaded {tmp.len} files"
shuffle(tmp)
for file in tmp:
fileList.addLast(file)
if fileList.len == 0:
return fail[string] "No files found"
let next = fileList.popFirst()
# Remove the current file after
ok next
proc getFileName(ip: var ImageProvider): OP[string] =
## Get the temporary file name of the next file to display
case ip.mode
of Mode.None:
return ip.getPlaceHolder()
of Mode.File:
return ip.getLocalFile()
of Mode.Foxes:
return ip.getFox()
of Mode.Inspiro:
return ip.getInspiro()
########################
# Exported procs
########################
proc next*(ip: var ImageProvider, maxWidth, maxHeight: int): OP[string] =
## Uses the image provider to get a new image ready to display.
## `width` and `height` should be the size of the window.
let r = ip.getFileName()
if not r.isOk: return r
var rawPixbuf = newPixbufFromFile(r.val)
# Resize the pixbuf to best fit on screen
let size = calcImageSize(maxWidth, maxHeight, rawPixbuf.width, rawPixbuf.height)
ip.log "Scale image to: ", size
let then = now()
var pixbuf = rawPixbuf.scaleSimple(size.width, size.height, InterpType.nearest)
let now = now()
ip.log "Image scaled. Time: ", (now - then).inMilliseconds, "ms"
# The pixbuf is written to disk and loaded again once because
# directly setting the image from a pixbuf will leak memory
let saved = pixbuf.savev(tmpFile, "png", @[])
if not saved:
return result.fail "Error while saving temporary image"
# GTK pixbuf leaks memory when not manually decreasing reference count
pixbuf.unref()
rawPixbuf.unref()
ok tmpFile
createDir(tmpDir)
randomize()

5
src/providers/foxes.nim Normal file
View File

@@ -0,0 +1,5 @@
import ../randopics
import gintro/[gdkpixbuf]
proc get(): Pixbuf =
discard

104
src/randopics.nim Normal file
View File

@@ -0,0 +1,104 @@
import os, httpClient, json, threadpool
import gintro/[gtk, glib, gobject, gio, gdkpixbuf]
var
window: ApplicationWindow
fullscreen = true
const
floofUrl = "https://randomfox.ca/floof/"
updateTime = 300
type ImageProvider =
tuple[
get: proc(): Pixbuf
]
proc downloadImage(): Pixbuf =
let client = newHttpClient()
let urlData = client.getContent(floofUrl)
let info = parseJson(urlData)
let imageData = client.getContent(info["image"].getStr)
let loader = newPixbufLoader()
discard loader.write(imageData)
loader.getPixbuf()
proc resizeImage(pixbuf: Pixbuf, maxWidth, maxHeight: int): Pixbuf =
var width, height: int
if (maxWidth > maxHeight):
height = maxHeight
width = ((pixbuf.width * height) / pixbuf.height).toInt
else:
width = maxWidth
height = ((pixbuf.height * width) / pixbuf.width).toInt
pixbuf.scaleSimple(width, height, InterpType.bilinear)
proc replaceImage(widget: Image, width, height: int) =
var pixbuf = downloadImage()
pixbuf = pixbuf.resizeImage(width, height)
widget.setFromPixbuf(pixbuf)
proc updateCommand(action: SimpleAction; parameter: Variant; widget: Image) =
var width, height: int
window.getSize(width, height)
replaceImage(widget, width, height)
proc toggleFullscreen(action: SimpleAction; parameter: Variant; window: ApplicationWindow) =
if fullscreen:
window.unfullscreen
else:
window.fullscreen
fullscreen = not fullscreen
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
app.quit()
proc runUpdater(window: ApplicationWindow, image: Image) =
echo "Start Updater"
var width, height: int
window.getSize(width, height)
spawn replaceImage(image, width, height)
proc appActivate(app: Application) =
window = newApplicationWindow(app)
window.title = "Randopics"
window.setKeepAbove(true)
let cssProvider = newCssProvider()
let data = "window { background: black; }"
discard cssProvider.loadFromData(data)
let styleContext = window.getStyleContext()
styleContext.addProvider(cssProvider, STYLE_PROVIDER_PRIORITY_USER)
let imageWidget = newImage()
window.add(imageWidget)
window.connect("show", runUpdater, imageWidget)
if fullscreen:
window.fullscreen
let fullscreenAction = newSimpleAction("fullscreen")
discard fullscreenAction.connect("activate", toggleFullscreen, window)
app.setAccelsForAction("win.fullscreen", "F")
window.actionMap.addAction(fullscreenAction)
let quitAction = newSimpleAction("quit")
discard quitAction.connect("activate", quit, app)
app.setAccelsForAction("win.quit", "Escape")
window.actionMap.addAction(quitAction)
let updateAction = newSimpleAction("update")
discard updateAction.connect("activate", updateCommand, imageWidget)
app.setAccelsForAction("win.update", "U")
window.actionMap.addAction(updateAction)
window.showAll
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", appActivate)
discard run(app)
when isMainModule:
main()

2
src/randopics.nim.cfg Normal file
View File

@@ -0,0 +1,2 @@
threads:on
d:ssl

View File

@@ -1,292 +0,0 @@
import os, options, strformat
import op
import gintro/[glib, gobject, gtk, gio]
import gintro/gdk except Window
import argparse except run
import providers, server, common
const
css = slurp("resources/app.css")
version = slurp("version")
helpString = [
"ESC\tClose program",
"H\tShow/Hide this help",
"F\tToggle fullscreen",
"U\tForce refresh"
].join("\n")
type
Args = ref object
fullscreen: bool ## Applicaion is show in fullscreen mode
verbose: bool ## More debug information in notification label
timeout: int ## Milliseconds between image refreshes
port: int ## Port to host the control server
var
imageProvider: ImageProvider ## Gets images from the chosen source
args: Args ## The parsed command line args
updateTimeout, serverTimeout: int ## ID of the timeouts for image updating and server checking
# Widgets
window: ApplicationWindow
label: Label
box: Box
# Server vor recieving commands from external tools
serverWorker: system.Thread[ServerArgs]
proc log(things: varargs[string, `$`]) =
if args.verbose:
echo things.join()
proc notify(label: Label, things: varargs[string, `$`]) =
## Shows the notification box in the lower left corner.
## If no message is passed, the box will be hidden
label.text = things.join()
if (label.text == ""):
box.hide
else:
box.show
proc newArgs(): Option[Args] =
let p = newParser("randopix"):
help(fmt"Version {version} - Display random images from different sources")
option("-m", "--mode", help="The image source mode.", choices=enumToStrings(Mode))
option("-d", "--directoy", help="Path to a directory with images for the 'file' mode")
option("-t", "--timeout", help="Seconds before the image is refreshed", default="300")
option("-p", "--port", help="Port over which the control server should be accessible", default="8080")
flag("-w", "--windowed", help="Do not start in fullscreen mode")
flag("-v", "--verbose", help="Show more information")
try:
let opts = p.parse(commandLineParams())
# Catch the help option. Do nothing more
if opts.help:
return
# Parse the starting mode
var startMode: Mode
try:
startMode= parseEnum[Mode](opts.mode)
except ValueError:
startMode = Mode.None
# Create the image provider
if opts.directoy != "":
imageProvider = newImageProvider(opts.verbose, startMode, opts.directoy)
else:
imageProvider = newImageProvider(opts.verbose, startMode)
## Timeout is given in seconds as an argument
var timeout = 3000
try:
timeout = opts.timeout.parseInt * 1000
except ValueError:
raise newException(UsageError, fmt"Invalid timeout value: {opts.timeout}")
return some(Args(
fullscreen: not opts.windowed,
verbose: opts.verbose,
timeout: timeout,
port: opts.port.parseInt))
except:
echo p.help
proc updateImage(image: Image): bool =
## Updates the UI with a new image
try:
if args.verbose: log "Refreshing..."
if imageProvider.mode == Mode.None:
log "No display mode"
label.notify "No mode selected"
var wWidth, wHeight: int
window.getSize(wWidth, wHeight)
let r = imageProvider.next(wWidth, wHeight)
result = r.isOk
if not r.isOk:
label.notify r.error
return
image.setFromFile(r.val)
if imageProvider.mode != Mode.None:
label.notify
except:
let
e = getCurrentException()
msg = getCurrentExceptionMsg()
log "Got exception ", repr(e), " with message ", msg
label.notify "Error while refreshing, retrying..."
return false
proc timedUpdate(image: Image): bool =
discard updateImage(image)
# Force garbage collection now. Otherwise the RAM will fill up until the GC is triggered.
GC_fullCollect()
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
return false
proc forceUpdate(action: SimpleAction; parameter: Variant; image: Image): void =
log "Refreshing..."
label.notify "Refreshing..."
if updateTimeout > 0:
discard updateTimeout.remove
updateTimeout = int(timeoutAdd(500, timedUpdate, image))
proc checkServerChannel(image: Image): bool =
## Check the channel from the control server for incomming commands
let tried = chan.tryRecv()
if tried.dataAvailable:
let msg: CommandMessage = tried.msg
log "Recieved command: ", msg.command
case msg.command
of cRefresh:
forceUpdate(nil, nil, image)
of cTimeout:
let val = msg.parameter.parseInt * 1000
log "Setting timeout to ", val
args.timeout = val
if updateTimeout > 0:
discard updateTimeout.remove
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
of cMode:
try:
let mode = parseEnum[Mode](msg.parameter)
imageProvider.mode = mode
forceUpdate(nil, nil, image)
log "Switching mode: ", mode
label.notify fmt"Switch Mode: {msg.parameter.capitalizeAscii()}"
except ValueError:
log "Invalid mode: ", msg.parameter
else:
log "Command ignored: ", msg.command
result = true
proc toggleFullscreen(action: SimpleAction; parameter: Variant; window: ApplicationWindow) =
## Fullscreen toggle event
if args.fullscreen:
window.unfullscreen
else:
window.fullscreen
args.fullscreen = not args.fullscreen
proc toggleHelp(action: SimpleAction; parameter: Variant; box: Box) =
if box.visible:
box.hide
else:
box.show
proc cleanUp(w: ApplicationWindow, app: Application) =
## Stop the control server and exit the GTK application
chan.close()
log "Server channel closed."
app.quit()
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
## Application quit event
cleanUp(window, app)
proc realizeWindow(window: ApplicationWindow, image: Image): void =
## Hides the mouse pointer for the application.
if args.fullscreen:
window.fullscreen
let cur = window.getDisplay().newCursorForDisplay(CursorType.blankCursor)
let win = window.getWindow()
win.setCursor(cur)
# Setting the inital image
# Fix 1 second timeout to make sure all other initialization has finished
updateTimeout = int(timeoutAdd(1000, timedUpdate, image))
proc appActivate(app: Application) =
# Parse arguments from the command line
let parsed = newArgs()
if parsed.isNone:
return
else:
args = parsed.get
window = newApplicationWindow(app)
window.title = "randopix"
window.setKeepAbove(false)
window.setDefaultSize(800, 600)
# Custom styling for e.g. the background color CSS data is in the "style.nim" module
let provider = newCssProvider()
discard provider.loadFromData(css)
addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_USER)
# Create all windgets we are gonna use
label = newLabel(fmt"Starting ('H' for help)...")
let spinner = newSpinner()
spinner.start()
box = newBox(Orientation.horizontal, 2)
box.halign = Align.`end`
box.valign = Align.`end`
box.packStart(spinner, true, true, 10)
box.packStart(label, true, true, 0)
let helpText = newLabel(helpString)
let helpBox = newBox(Orientation.vertical, 0)
helpBox.packStart(helpText, true, true, 0)
helpBox.halign = Align.start
helpBox.valign = Align.start
let container = newOverlay()
container.addOverlay(box)
container.addOverlay(helpBox)
window.add(container)
let image = newImage()
container.add(image)
## Connect the GTK signals to the procs
var action: SimpleAction
action = newSimpleAction("fullscreen")
discard action.connect("activate", toggleFullscreen, window)
app.setAccelsForAction("win.fullscreen", "F")
window.actionMap.addAction(action)
action = newSimpleAction("quit")
discard action.connect("activate", quit, app)
app.setAccelsForAction("win.quit", "Escape")
window.actionMap.addAction(action)
action = newSimpleAction("update")
discard action.connect("activate", forceUpdate, image)
app.setAccelsForAction("win.update", "U")
window.actionMap.addAction(action)
action = newSimpleAction("help")
discard action.connect("activate", toggleHelp, helpBox)
app.setAccelsForAction("win.help", "H")
window.actionMap.addAction(action)
window.connect("destroy", cleanUp, app)
window.connect("realize", realizeWindow, image)
window.showAll
# Help is only shown on demand
helpBox.hide
## open communication channel from the control server
chan.open()
## Start the server for handling incoming commands
let serverArgs = ServerArgs(verbose: args.verbose, port: args.port)
createThread(serverWorker, runServer, serverArgs)
serverTimeout = int(timeoutAdd(100, checkServerChannel, image))
when isMainModule:
let app = newApplication("org.luxick.randopix")
connect(app, "activate", appActivate)
discard run(app)

View File

@@ -1,3 +0,0 @@
threads:on
d:ssl
# gc:arc

View File

@@ -1,11 +0,0 @@
window {
background: black;
font-size: 30px;
}
box {
background-color: rgba(255, 255, 255, .75);
border: 2px solid gray;
border-radius: 5px;
margin: 10px;
padding: 10px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>randopix</title>
<link rel="stylesheet" type="text/css" href="/site.css">
<script src="/pixctrl.js" type="text/javascript"></script>
<script src="/script.js" type="text/javascript"></script>
</head>
<body class="star-bg">
<main>
<h1 class="center">Randopix Remote</h1>
<p>Control the image that is shown on this randopix.</p>
<div class="control">
<div class="control-info">Load the next image</div>
<div class="control-body">
<button type="button" class="btn" onclick="refresh()">Refresh Image Now!</button>
</div>
</div>
<div class="control">
<div class="control-info">Switch the image mode</div>
<div class="control-body">
<select id="modeselect"></select>
<button type="button" class="btn" onclick="js_setMode()">Set Mode</button>
</div>
</div>
<div class="control">
<div class="control-info">Adjust timespan between images</div>
<div class="control-body">
<input id="timeout" type="number">
<button type="button" class="btn" onclick="js_setTimeout()">Set Timeout</button>
</div>
</div>
<div class="social">
<a class="btn btn-mini" href="https://github.com/luxick/randopix">
<img src="/github.png"></img>
Source code and usage
</a>
</div>
</main>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,30 +0,0 @@
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
window.onload = () => {
const modes = getModes();
const modeselect = document.getElementById("modeselect");
modes.forEach(mode => {
let opt = document.createElement('option');
opt.value = mode;
opt.innerHTML = capitalize(mode)
modeselect.appendChild(opt);
});
}
function js_setTimeout() {
const elem = document.getElementById("timeout");
const timeout = parseInt(elem.value);
if (timeout < 1) {
console.error("timeout must be positive");
return;
}
setTimeout(timeout.toString());
elem.value = null;
}
function js_setMode() {
const modeselect = document.getElementById("modeselect");
switchMode(modeselect.value)
}

View File

@@ -1,157 +0,0 @@
@charset "utf-8";
/* CSS Document */
@font-face {
font-family: notcomicsans;
src: url("/not-comic-sans.woff") format('truetype');
font-weight: normal;
font-style: normal;
}
.star-bg {
background-color: #1c1d1b;
background-image: url("/stars.gif")
}
/* Chrome, Safari, Opera */
@-webkit-keyframes rainbow {
0%{color: orange;}
10%{color: purple;}
20%{color: red;}
30%{color: CadetBlue;}
40%{color: yellow;}
50%{color: coral;}
60%{color: green;}
70%{color: cyan;}
80%{color: DeepPink;}
90%{color: DodgerBlue;}
100%{color: orange;}
}
/* Internet Explorer */
@-ms-keyframes rainbow {
0%{color: orange;}
10%{color: purple;}
20%{color: red;}
30%{color: CadetBlue;}
40%{color: yellow;}
50%{color: coral;}
60%{color: green;}
70%{color: cyan;}
80%{color: DeepPink;}
90%{color: DodgerBlue;}
100%{color: orange;}
}
/* Standard Syntax */
@keyframes rainbow {
0%{color: orange;}
10%{color: purple;}
20%{color: red;}
30%{color: CadetBlue;}
40%{color: yellow;}
50%{color: coral;}
60%{color: green;}
70%{color: cyan;}
80%{color: DeepPink;}
90%{color: DodgerBlue;}
100%{color: orange;}
}
body {
font-family: notcomicsans;
color:#FFFF00;
display: flex;
flex-direction: column;
align-items: stretch;
font-size: 1.2rem;
}
main {
max-width: 900px;
align-self: center;
border: 1px solid gray;
border-radius: 5px;
background-image: url("/microfab.gif");
padding: 15px;
}
.control {
padding-bottom: 20px;
}
.control-info {
padding-bottom: 5px;
}
.control-body {
display: flex;
justify-content: space-around;
align-items: center;
}
.control-body > * {
margin-left: 5px;
margin-right: 5px;
}
h1 {
/* Chrome, Safari, Opera */
-webkit-animation: rainbow 5s infinite;
/* Internet Explorer */
-ms-animation: rainbow 5s infinite;
/* Standar Syntax */
animation: rainbow 5s infinite;
margin-top: 0;
}
button {
padding: 0;
border: none;
font: inherit;
color: inherit;
background-color: transparent;
/* show a hand cursor on hover; some argue that we
should keep the default arrow cursor for buttons */
cursor: pointer;
}
a:visited {
color: #ff0;
}
.btn {
text-align: center;
font-size: 1.3rem;
text-decoration: none;
display: block;
border: 2px solid darkgray;
border-radius: 5px;
padding: 0px 10px;
background-color: #1c1d1b;
}
.btn:hover {
color: #ff0;
fill: #ff0;
background-color: #0b0c0b;
}
.btn-mini {
font-size: 0.75rem;
border: 1px solid darkgray;
padding: 3px;
display: flex;
align-items: center;
}
.btn-mini > img {
width: 0.75rem;
height: 0.75rem;
margin-right: 5px;
}
.social {
margin-top: 20px;
display: inline-block;
}
.center {
text-align: center;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,87 +0,0 @@
import asyncdispatch, strutils, json, logging, os
import jester
import common
when defined(release):
proc slurpResources(): Table[string, string] {.compileTime.} =
## Include everything from the www dir into the binary.
## This way the final executable will not need an static web folder.
for item in walkDir("src/resources/www/", true):
if item.kind == pcFile:
result[item.path] = slurp("resources/www/" & item.path)
const resources = slurpResources()
const
contentTypes = {
".js": "text/javascript",
".css": "text/css"
}.toTable
type
ServerArgs* = object of RootObj
verbose*: bool
port*: int
var
chan*: Channel[CommandMessage]
verbose: bool
proc log(things: varargs[string, `$`]) =
if verbose:
echo things.join()
when defined(release):
## When in release mode, use resources includes in the binary.
## When developing use the files directly.
router getRouter:
get "/":
resp resources["index.html"]
get "/@resource":
try:
var cType: string
if contentTypes.hasKey(@"resource".splitFile.ext):
cType = contentTypes[@"resource".splitFile.ext]
resp resources[@"resource"], contentType=cType
except KeyError:
log "Resource not found: ", @"resource"
resp Http404
else:
router getRouter:
get "/":
resp readFile("src/resources/www/index.html")
get "/@resource":
try:
var cType: string
if contentTypes.hasKey(@"resource".splitFile.ext):
cType = contentTypes[@"resource".splitFile.ext]
resp readFile("src/resources/www/" & @"resource"), contentType=cType
except KeyError:
log "Resource not found: ", @"resource"
resp Http404
router postRouter:
post "/":
try:
log "Command from ", request.ip
let json = request.body.parseJson
let msg = json.to(CommandMessage)
log "Got message: ", $msg
# Pass command from client to main applicaiton
chan.send(msg)
resp Http200
except:
log "Error: ", getCurrentExceptionMsg()
router randopixRouter:
extend postRouter, ""
extend getRouter, ""
proc runServer*[ServerArgs](arg: ServerArgs) {.thread, nimcall.} =
verbose = arg.verbose
logging.setLogFilter(lvlInfo)
let port = Port(arg.port)
let settings = newSettings(port=port)
var server = initJester(randopixRouter, settings=settings)
server.serve()

View File

@@ -1 +0,0 @@
threads:on

View File

@@ -1 +0,0 @@
1.1.0