mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 09:09:30 -07:00
Compare commits
11 Commits
v5.9.1
...
marine-for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c70d965347 | ||
|
|
1faef1b589 | ||
|
|
5d891fb38f | ||
|
|
97e0fda709 | ||
|
|
7cf9dd6466 | ||
|
|
a44bd866ed | ||
|
|
21ef7f476a | ||
|
|
c5b715d631 | ||
|
|
dfd9facc79 | ||
|
|
5b926a358e | ||
|
|
ba1fbd7088 |
2
dist/index.html
vendored
2
dist/index.html
vendored
File diff suppressed because one or more lines are too long
2
dist/resources/ws.min.css
vendored
2
dist/resources/ws.min.css
vendored
File diff suppressed because one or more lines are too long
2
dist/resources/ws.min.js
vendored
2
dist/resources/ws.min.js
vendored
File diff suppressed because one or more lines are too long
494
package-lock.json
generated
494
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -187,7 +187,7 @@ const enterFullScreen = () => {
|
||||
|
||||
// change hover text and image
|
||||
const img = document.getElementById('ToggleFullScreen');
|
||||
img.src = 'images/nav/ic_fullscreen_exit_white_24dp_1x.png';
|
||||
img.src = 'images/nav/ic_fullscreen_exit_white_24dp_2x.png';
|
||||
img.title = 'Exit fullscreen';
|
||||
};
|
||||
|
||||
@@ -211,7 +211,7 @@ const exitFullscreen = () => {
|
||||
resize();
|
||||
// change hover text and image
|
||||
const img = document.getElementById('ToggleFullScreen');
|
||||
img.src = 'images/nav/ic_fullscreen_white_24dp_1x.png';
|
||||
img.src = 'images/nav/ic_fullscreen_white_24dp_2x.png';
|
||||
img.title = 'Enter fullscreen';
|
||||
};
|
||||
|
||||
@@ -290,37 +290,41 @@ const updateFullScreenNavigate = () => {
|
||||
};
|
||||
|
||||
const documentKeydown = (e) => {
|
||||
const code = (e.keyCode || e.which);
|
||||
|
||||
// 200ms repeat
|
||||
if ((Date.now() - documentKeydown.lastButton ?? 0) < 200) return false;
|
||||
documentKeydown.lastButton = Date.now();
|
||||
const { key } = e;
|
||||
|
||||
if (document.fullscreenElement || document.activeElement === document.body) {
|
||||
switch (code) {
|
||||
case 32: // Space
|
||||
switch (key) {
|
||||
case ' ': // Space
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigatePlayClick();
|
||||
return false;
|
||||
|
||||
case 39: // Right Arrow
|
||||
case 34: // Page Down
|
||||
case 'ArrowRight':
|
||||
case 'PageDown':
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigateNextClick();
|
||||
return false;
|
||||
|
||||
case 37: // Left Arrow
|
||||
case 33: // Page Up
|
||||
case 'ArrowLeft':
|
||||
case 'PageUp':
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigatePreviousClick();
|
||||
return false;
|
||||
|
||||
case 36: // Home
|
||||
case 'ArrowUp': // Home
|
||||
e.preventDefault();
|
||||
btnNavigateMenuClick();
|
||||
return false;
|
||||
|
||||
case 48: // Restart
|
||||
case '0': // "O" Restart
|
||||
btnNavigateRefreshClick();
|
||||
return false;
|
||||
|
||||
case 70: // F
|
||||
case 'F':
|
||||
case 'f':
|
||||
btnFullScreenClick();
|
||||
return false;
|
||||
|
||||
@@ -368,7 +372,6 @@ const btnGetGpsClick = async () => {
|
||||
txtAddress.value = `${round2(latitude, 4)}, ${round2(longitude, 4)}`;
|
||||
|
||||
doRedirectToGeometry({ y: latitude, x: longitude }, (point) => {
|
||||
console.log(point);
|
||||
const location = point.properties.relativeLocation.properties;
|
||||
// Save the query
|
||||
const query = `${location.city}, ${location.state}`;
|
||||
|
||||
@@ -22,7 +22,7 @@ class Almanac extends WeatherDisplay {
|
||||
}
|
||||
|
||||
async getData(_weatherParameters) {
|
||||
if (!super.getData(_weatherParameters)) return;
|
||||
const superResponse = super.getData(_weatherParameters);
|
||||
const weatherParameters = _weatherParameters ?? this.weatherParameters;
|
||||
|
||||
// get sun/moon data
|
||||
@@ -33,11 +33,13 @@ class Almanac extends WeatherDisplay {
|
||||
sun,
|
||||
moon,
|
||||
};
|
||||
// update status
|
||||
this.setStatus(STATUS.loaded);
|
||||
|
||||
// share data
|
||||
this.getDataCallback();
|
||||
|
||||
if (!superResponse) return;
|
||||
|
||||
// update status
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
calcSunMoonData(weatherParameters) {
|
||||
|
||||
@@ -51,7 +51,10 @@ class Hazards extends WeatherDisplay {
|
||||
|
||||
this.getDataCallback();
|
||||
|
||||
if (!superResult) return;
|
||||
if (!superResult) {
|
||||
this.setStatus(STATUS.loaded);
|
||||
return;
|
||||
}
|
||||
this.drawLongCanvas();
|
||||
}
|
||||
|
||||
@@ -72,6 +75,7 @@ class Hazards extends WeatherDisplay {
|
||||
|
||||
// no alerts, skip this display by setting timing to zero
|
||||
if (lines.length === 0) {
|
||||
this.setStatus(STATUS.loaded);
|
||||
this.timing.totalScreens = 0;
|
||||
this.setStatus(STATUS.loaded);
|
||||
return;
|
||||
@@ -88,8 +92,8 @@ class Hazards extends WeatherDisplay {
|
||||
for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep);
|
||||
// add the final 3 second delay
|
||||
this.timing.delay.push(150);
|
||||
this.calcNavTiming();
|
||||
this.setStatus(STATUS.loaded);
|
||||
this.calcNavTiming();
|
||||
}
|
||||
|
||||
drawCanvas() {
|
||||
|
||||
@@ -141,6 +141,7 @@ const parseForecast = async (data) => {
|
||||
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
|
||||
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
|
||||
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
|
||||
const waveHeight = expand(data.waveHeight.values);
|
||||
|
||||
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||
|
||||
@@ -152,6 +153,7 @@ const parseForecast = async (data) => {
|
||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||
skyCover: skyCover[idx],
|
||||
icon: icons[idx],
|
||||
waveHeight: waveHeight[idx],
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
return addPath('Clear-Wind-1994.gif');
|
||||
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing Snow.gif');
|
||||
|
||||
case 'cold':
|
||||
@@ -268,6 +269,7 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing-Snow.gif');
|
||||
|
||||
default:
|
||||
|
||||
139
server/scripts/modules/marineforecast.mjs
Normal file
139
server/scripts/modules/marineforecast.mjs
Normal file
@@ -0,0 +1,139 @@
|
||||
// display extended forecast graphically
|
||||
// technically uses the same data as the local forecast, we'll let the browser do the caching of that
|
||||
|
||||
import STATUS from './status.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import getHourlyForecast from './hourly.mjs';
|
||||
|
||||
class MarineForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Marine Forecast', false);
|
||||
// this.showOnProgress = false;
|
||||
|
||||
// set timings
|
||||
this.timing.totalScreens = 1;
|
||||
}
|
||||
|
||||
async getData() {
|
||||
if (!super.getData()) return;
|
||||
|
||||
const hourlyForecast = await getHourlyForecast(() => this.stillWaiting());
|
||||
if (hourlyForecast === undefined) {
|
||||
this.setStatus(STATUS.failed);
|
||||
return;
|
||||
}
|
||||
|
||||
// test for all wave heights = 0, no data for wave heights
|
||||
if (hourlyForecast.every((value) => !value.waveHeight)) {
|
||||
// total screens = 0 to skip this display
|
||||
this.totalScreens = 0;
|
||||
this.setStatus(STATUS.noData);
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = hourlyForecast;
|
||||
this.screenIndex = 0;
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
async drawCanvas() {
|
||||
super.drawCanvas();
|
||||
|
||||
// determine bounds
|
||||
// grab the first three or second set of three array elements
|
||||
const forecast = this.data.slice(0, 2);
|
||||
|
||||
// create each day template
|
||||
const days = forecast.map((Day) => {
|
||||
const fill = {};
|
||||
const waveHeight = Math.round(Day.waveHeight * 3.281);
|
||||
fill.date = Day.dayName;
|
||||
fill['wind-dir'] = Day.windDirection;
|
||||
fill['wind-speed'] = '10 - 15kts';
|
||||
fill['wave-height'] = `${waveHeight}'`;
|
||||
fill['wave-desc'] = waveDesc(waveHeight);
|
||||
|
||||
const { low } = Day;
|
||||
if (low !== undefined) {
|
||||
fill['value-lo'] = Math.round(low);
|
||||
}
|
||||
const { high } = Day;
|
||||
fill['value-hi'] = Math.round(high);
|
||||
fill.condition = Day.text;
|
||||
|
||||
// draw the icon
|
||||
fill['wave-icon'] = { type: 'img', src: waveImage('') };
|
||||
|
||||
// return the filled template
|
||||
return this.fillTemplate('day', fill);
|
||||
});
|
||||
|
||||
// empty and update the container
|
||||
const dayContainer = this.elem.querySelector('.day-container');
|
||||
dayContainer.innerHTML = '';
|
||||
dayContainer.append(...days);
|
||||
this.finishDraw();
|
||||
}
|
||||
}
|
||||
|
||||
const waveImage = (conditions) => {
|
||||
const color = 'rgb(172, 165, 251)';
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 150;
|
||||
canvas.height = 20;
|
||||
const context = canvas.getContext('2d');
|
||||
context.imageSmoothingEnabled = false;
|
||||
|
||||
let y = 0;
|
||||
let r = 35;
|
||||
let arc1 = Math.PI * 0.3;
|
||||
let arc2 = Math.PI * 0.7;
|
||||
|
||||
switch (conditions) {
|
||||
case 'CHOPPY':
|
||||
y = -10;
|
||||
arc1 = Math.PI * 0.2;
|
||||
arc2 = Math.PI * 0.8;
|
||||
r = 25;
|
||||
break;
|
||||
|
||||
case 'ROUGH':
|
||||
y = -5;
|
||||
arc1 = Math.PI * 0.1;
|
||||
arc2 = Math.PI * 0.9;
|
||||
r = 20;
|
||||
break;
|
||||
|
||||
case 'LIGHT':
|
||||
default:
|
||||
y = -20;
|
||||
arc1 = Math.PI * 0.3;
|
||||
arc2 = Math.PI * 0.7;
|
||||
r = 35;
|
||||
break;
|
||||
}
|
||||
|
||||
context.beginPath();
|
||||
context.arc(35, y, r, arc1, arc2);
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 4;
|
||||
context.stroke();
|
||||
context.beginPath();
|
||||
context.arc(75, y, r, arc1, arc2);
|
||||
context.stroke();
|
||||
context.beginPath();
|
||||
context.arc(115, y, r, arc1, arc2);
|
||||
context.stroke();
|
||||
|
||||
return canvas.toDataURL();
|
||||
};
|
||||
|
||||
const waveDesc = (waveHeight) => {
|
||||
if (waveHeight > 7) return 'ROUGH';
|
||||
if (waveHeight > 4) return 'CHOPPY';
|
||||
return 'LIGHT';
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new MarineForecast(11, 'marine-forecast'));
|
||||
@@ -175,6 +175,7 @@ const navTo = (direction) => {
|
||||
if (!firstDisplay) return;
|
||||
|
||||
firstDisplay.navNext(msg.command.firstFrame);
|
||||
firstDisplay.showCanvas();
|
||||
return;
|
||||
}
|
||||
if (direction === msg.command.nextFrame) currentDisplay().navNext();
|
||||
@@ -218,11 +219,11 @@ const setPlaying = (newValue) => {
|
||||
if (playing) {
|
||||
noSleep(true);
|
||||
playButton.title = 'Pause';
|
||||
playButton.src = 'images/nav/ic_pause_white_24dp_1x.png';
|
||||
playButton.src = 'images/nav/ic_pause_white_24dp_2x.png';
|
||||
} else {
|
||||
noSleep(false);
|
||||
playButton.title = 'Play';
|
||||
playButton.src = 'images/nav/ic_play_arrow_white_24dp_1x.png';
|
||||
playButton.src = 'images/nav/ic_play_arrow_white_24dp_2x.png';
|
||||
}
|
||||
// if we're playing and on the progress screen jump to the next screen
|
||||
if (!progress) return;
|
||||
@@ -377,7 +378,7 @@ const stopAutoRefreshTimer = () => {
|
||||
|
||||
const refreshCheck = () => {
|
||||
// Time has elapsed.
|
||||
if (AutoRefreshCountMs >= AUTO_REFRESH_TIME_MS) {
|
||||
if (AutoRefreshCountMs >= AUTO_REFRESH_TIME_MS && isPlaying()) {
|
||||
loadTwcData();
|
||||
return true;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
98
server/styles/scss/_marine-forecast.scss
Normal file
98
server/styles/scss/_marine-forecast.scss
Normal file
@@ -0,0 +1,98 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
|
||||
#marine-forecast-html.weather-display {
|
||||
background-image: url('../images/BackGround8_1.png');
|
||||
}
|
||||
|
||||
.weather-display .main.marine-forecast {
|
||||
font-family: 'Star4000';
|
||||
font-size: 24pt;
|
||||
@include u.text-shadow();
|
||||
|
||||
.advisory {
|
||||
width: 100%;
|
||||
height: 90px;
|
||||
overflow: hidden;
|
||||
|
||||
.advisory-text {
|
||||
border: 4px solid black;
|
||||
width: 75%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
.headers {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
.winds {
|
||||
text-align: right;
|
||||
width: 150px;
|
||||
margin-top: 42px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.day-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.day {
|
||||
padding: 5px;
|
||||
width: 165px;
|
||||
display: inline-block;
|
||||
margin: 0px 15px;
|
||||
text-align: center;
|
||||
|
||||
.date {
|
||||
color: c.$title-color;
|
||||
}
|
||||
|
||||
.wave {
|
||||
border: 4px solid #b09ffb;
|
||||
|
||||
.wave-icon {
|
||||
height: 20px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.temperatures {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
|
||||
.temperature-block {
|
||||
display: inline-block;
|
||||
width: 44%;
|
||||
vertical-align: top;
|
||||
|
||||
>div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-family: 'Star4000 Large';
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&.lo .label {
|
||||
color: c.$extended-low;
|
||||
}
|
||||
|
||||
&.hi .label {
|
||||
color: c.$title-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,8 +213,7 @@ button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#imgPause1x,
|
||||
#imgPause2x {
|
||||
#imgPause1x {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
@@ -318,6 +317,10 @@ button {
|
||||
margin-bottom: 15px;
|
||||
@include u.status-colors();
|
||||
|
||||
.press-here {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
|
||||
.loading,
|
||||
@@ -326,7 +329,7 @@ button {
|
||||
}
|
||||
|
||||
.press-here {
|
||||
color: hsl(120, 100%, 30%);
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -343,10 +346,6 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.press-here {
|
||||
color: black;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
max-width: 300px;
|
||||
@@ -363,7 +362,7 @@ button {
|
||||
}
|
||||
|
||||
#divTwcBottom img {
|
||||
zoom: 150%;
|
||||
zoom: 75%;
|
||||
}
|
||||
|
||||
#divTwc:fullscreen {
|
||||
|
||||
@@ -11,4 +11,5 @@
|
||||
@import 'radar';
|
||||
@import 'regional-forecast';
|
||||
@import 'almanac';
|
||||
@import 'hazards';
|
||||
@import 'hazards';
|
||||
@import 'marine-forecast';
|
||||
@@ -42,7 +42,7 @@
|
||||
<script type="module" src="scripts/modules/regionalforecast.mjs"></script>
|
||||
<script type="module" src="scripts/modules/travelforecast.mjs"></script>
|
||||
<script type="module" src="scripts/modules/progress.mjs"></script>
|
||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||
<script type="module" src="scripts/modules/marineforecast.mjs"></script>
|
||||
<script type="module" src="scripts/index.mjs"></script>
|
||||
|
||||
<!-- data -->
|
||||
@@ -68,11 +68,6 @@
|
||||
<div id="divLat"></div>
|
||||
<div id="divLng"></div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<img id="imgPause1x" src="images/nav/ic_pause_white_24dp_1x.png" />
|
||||
<img id="imgPause2x" src="images/nav/ic_pause_white_24dp_2x.png" />
|
||||
<div id="version" style="display:none">
|
||||
<%- version %>
|
||||
</div>
|
||||
@@ -112,6 +107,9 @@
|
||||
</div>
|
||||
<div id="almanac-html" class="weather-display">
|
||||
<%- include('partials/almanac.ejs') %>
|
||||
</div>
|
||||
<div id="marine-forecast-html" class="weather-display">
|
||||
<%- include('partials/marine-forecast.ejs') %>
|
||||
</div>
|
||||
<div id="extended-forecast-html" class="weather-display">
|
||||
<%- include('partials/extended-forecast.ejs') %>
|
||||
@@ -125,16 +123,16 @@
|
||||
</div>
|
||||
<div id="divTwcBottom">
|
||||
<div id="divTwcBottomLeft">
|
||||
<img id="NavigateMenu" class="navButton" src="images/nav/ic_menu_white_24dp_1x.png" title="Menu" />
|
||||
<img id="NavigatePrevious" class="navButton" src="images/nav/ic_skip_previous_white_24dp_1x.png" title="Previous" />
|
||||
<img id="NavigateNext" class="navButton" src="images/nav/ic_skip_next_white_24dp_1x.png" title="Next" />
|
||||
<img id="NavigatePlay" class="navButton" src="images/nav/ic_play_arrow_white_24dp_1x.png" title="Play" />
|
||||
<img id="NavigateMenu" class="navButton" src="images/nav/ic_menu_white_24dp_2x.png" title="Menu" />
|
||||
<img id="NavigatePrevious" class="navButton" src="images/nav/ic_skip_previous_white_24dp_2x.png" title="Previous" />
|
||||
<img id="NavigateNext" class="navButton" src="images/nav/ic_skip_next_white_24dp_2x.png" title="Next" />
|
||||
<img id="NavigatePlay" class="navButton" src="images/nav/ic_play_arrow_white_24dp_2x.png" title="Play" />
|
||||
</div>
|
||||
<div id="divTwcBottomMiddle">
|
||||
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_1x.png" title="Refresh" />
|
||||
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_2x.png" title="Refresh" />
|
||||
</div>
|
||||
<div id="divTwcBottomRight">
|
||||
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_1x.png" title="Enter Fullscreen" />
|
||||
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_2x.png" title="Enter Fullscreen" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
23
views/partials/marine-forecast.ejs
Normal file
23
views/partials/marine-forecast.ejs
Normal file
@@ -0,0 +1,23 @@
|
||||
<%- include('header.ejs', { title: 'Marine Forecast' , hasTime: true }) %>
|
||||
<div class="main has-scroll marine-forecast">
|
||||
<div class="advisory">
|
||||
<div class="advisory-text">Small Craft Advisory</div>
|
||||
</div>
|
||||
<div class="headers">
|
||||
<div class="winds">WINDS:</div>
|
||||
<div class="winds">WAVES:</div>
|
||||
</div>
|
||||
<div class="day-container">
|
||||
<div class="day template">
|
||||
<div class="date"></div>
|
||||
<div class="wind-dir"></div>
|
||||
<div class="wind-speed"></div>
|
||||
<div class="wave">
|
||||
<div class="wave-height"></div>
|
||||
<div class="wave-icon"><img src="" /></div>
|
||||
<div class="wave-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('scroll.ejs') %>
|
||||
@@ -51,5 +51,9 @@
|
||||
"editor.defaultFormatter": "j69.ejs-beautify"
|
||||
},
|
||||
"files.exclude": {},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user