Improved web GUI & added stop selector
This commit is contained in:
parent
33b6eaa641
commit
caf546ae6c
|
@ -22,7 +22,7 @@ class API:
|
||||||
def __init__(self, main):
|
def __init__(self, main):
|
||||||
|
|
||||||
@route('/departures/<stop_id>')
|
@route('/departures/<stop_id>')
|
||||||
def departures(stop_id: int):
|
def departures(stop_id: str):
|
||||||
resp = []
|
resp = []
|
||||||
for d in Departure.get(stop_id):
|
for d in Departure.get(stop_id):
|
||||||
resp.append(d.json())
|
resp.append(d.json())
|
||||||
|
@ -33,6 +33,10 @@ class API:
|
||||||
def stop():
|
def stop():
|
||||||
return {'stops': main.config["stops"]}
|
return {'stops': main.config["stops"]}
|
||||||
|
|
||||||
|
@get("/static/<file>")
|
||||||
|
def index(file: str):
|
||||||
|
return static_file(file, root="static")
|
||||||
|
|
||||||
@get("/")
|
@get("/")
|
||||||
def static():
|
def static():
|
||||||
return static_file("index.html", root="static")
|
return static_file("index.html", root="static")
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Departure:
|
||||||
resp = []
|
resp = []
|
||||||
for id in Departure.storage:
|
for id in Departure.storage:
|
||||||
d = Departure.storage[id]
|
d = Departure.storage[id]
|
||||||
if d.stop_id == int(stop_id):
|
if d.stop_id == stop_id:
|
||||||
resp.append(d)
|
resp.append(d)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -42,8 +42,6 @@ class Departure:
|
||||||
departure = (parser.parse(departure)).timestamp()
|
departure = (parser.parse(departure)).timestamp()
|
||||||
if -(datetime.now().timestamp() - (departure + delay*60))/60 <= -1:
|
if -(datetime.now().timestamp() - (departure + delay*60))/60 <= -1:
|
||||||
return
|
return
|
||||||
if len(last_stop) >= 21:
|
|
||||||
last_stop = last_stop[:20].strip() + "..."
|
|
||||||
self.did = did
|
self.did = did
|
||||||
self.stop_id = stop_id
|
self.stop_id = stop_id
|
||||||
self.id = Departure.get_id(stop_id)
|
self.id = Departure.get_id(stop_id)
|
||||||
|
@ -72,7 +70,10 @@ class Departure:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.id}|{self.type}|{self.line}|{self.last_stop}|{(self.get_departure()*10):.0f}"
|
last_stop = self.last_stop
|
||||||
|
if len(last_stop) >= 21:
|
||||||
|
last_stop = last_stop[:20].strip() + "..."
|
||||||
|
return f"{self.id}|{self.type}|{self.line}|{last_stop}|{(self.get_departure()*10):.0f}"
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -35,12 +35,12 @@ class LoraController:
|
||||||
data = requests.post(url, verify=False, headers=headers, data=json.dumps(data)).json()
|
data = requests.post(url, verify=False, headers=headers, data=json.dumps(data)).json()
|
||||||
self.token = data["token"]
|
self.token = data["token"]
|
||||||
|
|
||||||
def new(self, id: int, stop_id: int):
|
def new(self, id: int, stop_id: str):
|
||||||
self.devices.append(LoraDevice(self, id, stop_id))
|
self.devices.append(LoraDevice(self, id, stop_id))
|
||||||
|
|
||||||
class LoraDevice:
|
class LoraDevice:
|
||||||
|
|
||||||
def __init__(self, controller: LoraController, deveui: int, stop_id: int):
|
def __init__(self, controller: LoraController, deveui: int, stop_id: str):
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.id = deveui
|
self.id = deveui
|
||||||
self.stop_id = stop_id
|
self.stop_id = stop_id
|
||||||
|
|
|
@ -23,13 +23,13 @@ class Main:
|
||||||
lora_controller = LoraController(self)
|
lora_controller = LoraController(self)
|
||||||
lora_controller.generate_token()
|
lora_controller.generate_token()
|
||||||
for d in self.config["devices"]:
|
for d in self.config["devices"]:
|
||||||
lora_controller.new(d["id"], d["stop_id"])
|
lora_controller.new(d["id"], str(d["stop_id"]))
|
||||||
self.controller = lora_controller
|
self.controller = lora_controller
|
||||||
self.thread = threading.Thread(target=self.update_loop)
|
self.thread = threading.Thread(target=self.update_loop)
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
self.api = API(self)
|
self.api = API(self)
|
||||||
|
|
||||||
def fetch(self, stop_id, limit):
|
def fetch(self, stop_id: str, limit: int):
|
||||||
|
|
||||||
url = "https://jizdnirady.pmdp.cz/odjezdy/vyhledat"
|
url = "https://jizdnirady.pmdp.cz/odjezdy/vyhledat"
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ class Main:
|
||||||
|
|
||||||
if refetch == 0:
|
if refetch == 0:
|
||||||
for s in self.config["stops"]:
|
for s in self.config["stops"]:
|
||||||
self.fetch(s["id"], 15)
|
self.fetch(f"{s}", 15)
|
||||||
for d in self.controller.devices:
|
for d in self.controller.devices:
|
||||||
d.get_updated_departures()
|
d.get_updated_departures()
|
||||||
|
|
||||||
|
|
57
server/static/departure-list.js
Normal file
57
server/static/departure-list.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
|
||||||
|
const { createApp } = Vue;
|
||||||
|
let app = createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
top: 0,
|
||||||
|
interval: null,
|
||||||
|
stop_id: null,
|
||||||
|
stops: [],
|
||||||
|
departures: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async set() {
|
||||||
|
localStorage.setItem("favstop", app.$data.stop_id);
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
if(app.$data.stop_id) app.$data.departures = (await api("/departures/"+app.$data.stop_id)).departures;
|
||||||
|
},
|
||||||
|
async setup() {
|
||||||
|
app.$data.stops = (await api("/stops")).stops;
|
||||||
|
let ids = Object.keys(app.$data.stops);
|
||||||
|
if(ids.length < 1) {
|
||||||
|
alert("Žádné zastávky nejsou k dispozici!");
|
||||||
|
if(app.$data.interval) clearTimeout(app.$data.interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(localStorage.getItem("favstop")) {
|
||||||
|
if(ids.indexOf(localStorage.getItem("favstop")) != -1) app.$data.stop_id = localStorage.getItem("favstop");
|
||||||
|
}
|
||||||
|
if(!app.$data.stop_id) app.$data.stop_id = ids[0];
|
||||||
|
window.addEventListener("scroll", () => {
|
||||||
|
app.$data.top = document.querySelector('html').scrollTop;
|
||||||
|
});
|
||||||
|
app.$data.interval = setInterval(app.update, 5000);
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#app');
|
||||||
|
async function api(url) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if(this.readyState !== 4) return;
|
||||||
|
if(!this.responseText) reject(this);
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(this.responseText));
|
||||||
|
} catch(e) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.open("GET", url, true);
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
app.setup();
|
|
@ -2,43 +2,23 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Odjezdová tabule</title>
|
<title>Odjezdová tabule</title>
|
||||||
<style>
|
<link href="/static/style.css" rel="stylesheet">
|
||||||
|
|
||||||
:root {
|
|
||||||
--site-bg: #1C1C1C;
|
|
||||||
--site-alt-bg: #272727;
|
|
||||||
}
|
|
||||||
body {background-color: var(--site-bg); color: #fffd; margin: 0; margin-top: 35px; font-family: sans-serif; box-sizing: border-box;}
|
|
||||||
|
|
||||||
.container {max-width: 700px; padding: 25px; margin: auto;}
|
|
||||||
.departure-grid {display: grid; grid-template-columns: 30px 1fr max-content max-content; gap: 10px 20px; line-height: 31px;}
|
|
||||||
|
|
||||||
.header {line-height: 100%; opacity: .5; margin-bottom: 10px; font-size: 90%;}
|
|
||||||
|
|
||||||
.line {background-color: var(--site-alt-bg); border-radius: 3px; box-shadow: 0 0 5px 0 #0004; text-align: center; font-weight: 700; width: 30px; height: 30px; color: #fff; text-shadow: 0 0 15px #000;}
|
|
||||||
.last-stop {font-weight: 700;}
|
|
||||||
.departure {text-align: right;}
|
|
||||||
.delay {text-align: right; color: #ff6e6e; font-size: 80%;}
|
|
||||||
|
|
||||||
.line.type1 {background-color: #F0BE32;}
|
|
||||||
.line.type2 {background-color: #1E9641;}
|
|
||||||
.line.type3 {background-color: #CD2837;}
|
|
||||||
|
|
||||||
h3 {color: #fff;}
|
|
||||||
|
|
||||||
.nav {position: fixed; top: 0; left: 0; width: 100%; z-index: 10;}
|
|
||||||
.nav .inner {background: linear-gradient(to bottom, var(--site-bg) 70%, transparent 100%); padding: 5px 25px; margin: auto; max-width: 700px; padding-bottom: 25px; transition: padding .1s;}
|
|
||||||
.nav.top .inner {padding-bottom: 0px;}
|
|
||||||
.nav .inner::after {content: ""; position: fixed; bottom: 0; left: 0; height: 20px; width: 100%; background: linear-gradient(to top, var(--site-bg) 0%, transparent 100%)}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container" id="app">
|
<div class="container" id="app">
|
||||||
<div class="nav" :class="{'top': top == 0}">
|
<div class="nav" :class="{'top': top == 0}">
|
||||||
<div class="inner" v-if="stop">
|
<div class="inner" v-if="stop_id && stops[stop_id]">
|
||||||
<h3>{{ stop.name }}</h3>
|
<h3>
|
||||||
|
<span>{{ stops[stop_id].name }}</span>
|
||||||
|
<svg class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
|
||||||
|
</svg>
|
||||||
|
</h3>
|
||||||
|
<select v-model="stop_id" @change="set">
|
||||||
|
<option v-for="stop, id in stops" :value="id">{{ stop.name }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="departure-grid">
|
<div class="departure-grid">
|
||||||
|
@ -56,60 +36,6 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script src="/static/departure-list.js"></script>
|
||||||
const { createApp } = Vue;
|
|
||||||
let app = createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
top: 0,
|
|
||||||
interval: null,
|
|
||||||
stop: null,
|
|
||||||
stops: [],
|
|
||||||
departures: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async update() {
|
|
||||||
if(app.$data.stop) app.$data.departures = (await api("/departures/"+app.$data.stop.id)).departures;
|
|
||||||
},
|
|
||||||
async setup() {
|
|
||||||
app.$data.stops = (await api("/stops")).stops;
|
|
||||||
if(app.$data.stops.length < 1) {
|
|
||||||
alert("Žádné zastávky nejsou k dispozici!");
|
|
||||||
if(app.$data.interval) clearTimeout(app.$data.interval);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(localStorage.getItem("favstop")) {
|
|
||||||
app.$data.stops.forEach(stop => {
|
|
||||||
if(stop.id == localStorage.getItem("favstop")) app.$data.stop = stop;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if(!app.$data.stop) app.$data.stop = app.$data.stops[0];
|
|
||||||
window.addEventListener("scroll", () => {
|
|
||||||
app.$data.top = document.querySelector('html').scrollTop;
|
|
||||||
});
|
|
||||||
app.$data.interval = setInterval(app.update, 5000);
|
|
||||||
app.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).mount('#app');
|
|
||||||
async function api(url) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
if(this.readyState !== 4) return;
|
|
||||||
if(!this.responseText) reject(this);
|
|
||||||
try {
|
|
||||||
resolve(JSON.parse(this.responseText));
|
|
||||||
} catch(e) {
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.open("GET", url, true);
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
app.setup();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
31
server/static/style.css
Normal file
31
server/static/style.css
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
:root {
|
||||||
|
--site-bg: #1C1C1C;
|
||||||
|
--site-alt-bg: #272727;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {box-sizing: border-box;}
|
||||||
|
|
||||||
|
body {background-color: var(--site-bg); color: #fffd; margin: 0; margin-top: 35px; font-family: sans-serif;}
|
||||||
|
|
||||||
|
.container {max-width: 700px; padding: 25px; margin: auto;}
|
||||||
|
.departure-grid {display: grid; grid-template-columns: 30px 1fr max-content max-content; gap: 10px 20px; line-height: 31px;}
|
||||||
|
|
||||||
|
.header {line-height: 100%; opacity: .5; margin-bottom: 10px; font-size: 90%;}
|
||||||
|
|
||||||
|
.line {background-color: var(--site-alt-bg); border-radius: 3px; box-shadow: 0 0 5px 0 #0004; text-align: center; font-weight: 700; width: 30px; height: 30px; color: #fff; text-shadow: 0 0 15px #000;}
|
||||||
|
.last-stop {font-weight: 700;}
|
||||||
|
.departure {text-align: right;}
|
||||||
|
.delay {text-align: right; color: #ff6e6e; font-size: 80%;}
|
||||||
|
|
||||||
|
.line.type1 {background-color: #F0BE32;}
|
||||||
|
.line.type2 {background-color: #1E9641;}
|
||||||
|
.line.type3 {background-color: #CD2837;}
|
||||||
|
|
||||||
|
h3 {color: #fff; display: flex; align-items: center; gap: 5px;}
|
||||||
|
.icon {fill: #fff5; width: 24px; height: 24px;}
|
||||||
|
|
||||||
|
.nav {position: fixed; top: 0; left: 0; width: 100%; z-index: 10;}
|
||||||
|
.nav .inner {background: linear-gradient(to bottom, var(--site-bg) 70%, transparent 100%); padding: 5px 25px; margin: auto; max-width: 700px; padding-bottom: 25px; transition: padding .1s; position: relative;}
|
||||||
|
.nav.top .inner {padding-bottom: 0px;}
|
||||||
|
.nav .inner::after {content: ""; position: fixed; bottom: 0; left: 0; height: 20px; width: 100%; background: linear-gradient(to top, var(--site-bg) 0%, transparent 100%)}
|
||||||
|
.nav .inner select {opacity: 0; float: left; position: relative; top: -60px; height: 50px; width: 100%; cursor: pointer;}
|
Loading…
Reference in a new issue