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):
|
||||
|
||||
@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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
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>
|
||||
<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
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