Improved web GUI & added stop selector

This commit is contained in:
Filip Znachor 2022-12-14 19:28:38 +01:00
parent 33b6eaa641
commit caf546ae6c
7 changed files with 116 additions and 97 deletions

View file

@ -22,7 +22,7 @@ class API:
def __init__(self, main):
@route('/departures/<stop_id>')
def departures(stop_id: int):
def departures(stop_id: str):
resp = []
for d in Departure.get(stop_id):
resp.append(d.json())
@ -33,6 +33,10 @@ class API:
def stop():
return {'stops': main.config["stops"]}
@get("/static/<file>")
def index(file: str):
return static_file(file, root="static")
@get("/")
def static():
return static_file("index.html", root="static")

View file

@ -12,7 +12,7 @@ class Departure:
resp = []
for id in Departure.storage:
d = Departure.storage[id]
if d.stop_id == int(stop_id):
if d.stop_id == stop_id:
resp.append(d)
return resp
@ -42,8 +42,6 @@ class Departure:
departure = (parser.parse(departure)).timestamp()
if -(datetime.now().timestamp() - (departure + delay*60))/60 <= -1:
return
if len(last_stop) >= 21:
last_stop = last_stop[:20].strip() + "..."
self.did = did
self.stop_id = stop_id
self.id = Departure.get_id(stop_id)
@ -72,7 +70,10 @@ class Departure:
return None
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):
return {

View file

@ -35,12 +35,12 @@ class LoraController:
data = requests.post(url, verify=False, headers=headers, data=json.dumps(data)).json()
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))
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.id = deveui
self.stop_id = stop_id

View file

@ -23,13 +23,13 @@ class Main:
lora_controller = LoraController(self)
lora_controller.generate_token()
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.thread = threading.Thread(target=self.update_loop)
self.thread.start()
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"
@ -77,7 +77,7 @@ class Main:
if refetch == 0:
for s in self.config["stops"]:
self.fetch(s["id"], 15)
self.fetch(f"{s}", 15)
for d in self.controller.devices:
d.get_updated_departures()

View 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();

View file

@ -2,43 +2,23 @@
<html>
<head>
<title>Odjezdová tabule</title>
<style>
: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>
<link href="/static/style.css" rel="stylesheet">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div class="container" id="app">
<div class="nav" :class="{'top': top == 0}">
<div class="inner" v-if="stop">
<h3>{{ stop.name }}</h3>
<div class="inner" v-if="stop_id && stops[stop_id]">
<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 class="departure-grid">
@ -56,60 +36,6 @@
</template>
</div>
</div>
<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>
<script src="/static/departure-list.js"></script>
</body>
</html>

31
server/static/style.css Normal file
View 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;}