mirror of
https://github.com/pawelmalak/flame.git
synced 2026-03-10 14:33:11 +08:00
Client: Implemented new config system
This commit is contained in:
@@ -11,9 +11,10 @@ import {
|
||||
|
||||
// Typescript
|
||||
import {
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
SettingsForm,
|
||||
OtherSettingsForm,
|
||||
} from '../../../interfaces';
|
||||
|
||||
// UI
|
||||
@@ -22,50 +23,29 @@ import Button from '../../UI/Buttons/Button/Button';
|
||||
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
import { otherSettingsTemplate, inputHandler } from '../../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
updateConfig: (formData: SettingsForm) => void;
|
||||
updateConfig: (formData: OtherSettingsForm) => void;
|
||||
sortApps: () => void;
|
||||
sortCategories: () => void;
|
||||
loading: boolean;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
const { config } = props;
|
||||
|
||||
// Initial state
|
||||
const [formData, setFormData] = useState<SettingsForm>({
|
||||
customTitle: document.title,
|
||||
pinAppsByDefault: 1,
|
||||
pinCategoriesByDefault: 1,
|
||||
hideHeader: 0,
|
||||
hideApps: 0,
|
||||
hideCategories: 0,
|
||||
useOrdering: 'createdAt',
|
||||
appsSameTab: 0,
|
||||
bookmarksSameTab: 0,
|
||||
dockerApps: 1,
|
||||
dockerHost: 'localhost',
|
||||
kubernetesApps: 1,
|
||||
unpinStoppedApps: 1,
|
||||
});
|
||||
const [formData, setFormData] = useState<OtherSettingsForm>(
|
||||
otherSettingsTemplate
|
||||
);
|
||||
|
||||
// Get config
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
customTitle: searchConfig('customTitle', 'Flame'),
|
||||
pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
|
||||
pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
|
||||
hideHeader: searchConfig('hideHeader', 0),
|
||||
hideApps: searchConfig('hideApps', 0),
|
||||
hideCategories: searchConfig('hideCategories', 0),
|
||||
useOrdering: searchConfig('useOrdering', 'createdAt'),
|
||||
appsSameTab: searchConfig('appsSameTab', 0),
|
||||
bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
|
||||
dockerApps: searchConfig('dockerApps', 0),
|
||||
dockerHost: searchConfig('dockerHost', 'localhost'),
|
||||
kubernetesApps: searchConfig('kubernetesApps', 0),
|
||||
unpinStoppedApps: searchConfig('unpinStoppedApps', 0),
|
||||
...config,
|
||||
});
|
||||
}, [props.loading]);
|
||||
|
||||
@@ -87,17 +67,13 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
// Input handler
|
||||
const inputChangeHandler = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
isNumber?: boolean
|
||||
options?: { isNumber?: boolean; isBool?: boolean }
|
||||
) => {
|
||||
let value: string | number = e.target.value;
|
||||
|
||||
if (isNumber) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: value,
|
||||
inputHandler<OtherSettingsForm>({
|
||||
e,
|
||||
options,
|
||||
setStateHandler: setFormData,
|
||||
state: formData,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -126,8 +102,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="pinAppsByDefault"
|
||||
name="pinAppsByDefault"
|
||||
value={formData.pinAppsByDefault}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.pinAppsByDefault ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -140,8 +116,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="pinCategoriesByDefault"
|
||||
name="pinCategoriesByDefault"
|
||||
value={formData.pinCategoriesByDefault}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.pinCategoriesByDefault ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -165,8 +141,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="appsSameTab"
|
||||
name="appsSameTab"
|
||||
value={formData.appsSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.appsSameTab ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -177,8 +153,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="bookmarksSameTab"
|
||||
name="bookmarksSameTab"
|
||||
value={formData.bookmarksSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.bookmarksSameTab ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -192,8 +168,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="hideHeader"
|
||||
name="hideHeader"
|
||||
value={formData.hideHeader}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.hideHeader ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -204,8 +180,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="hideApps"
|
||||
name="hideApps"
|
||||
value={formData.hideApps}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.hideApps ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -216,8 +192,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="hideCategories"
|
||||
name="hideCategories"
|
||||
value={formData.hideCategories}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.hideCategories ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -242,8 +218,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="dockerApps"
|
||||
name="dockerApps"
|
||||
value={formData.dockerApps}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.dockerApps ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -256,8 +232,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="unpinStoppedApps"
|
||||
name="unpinStoppedApps"
|
||||
value={formData.unpinStoppedApps}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.unpinStoppedApps ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -271,8 +247,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="kubernetesApps"
|
||||
name="kubernetesApps"
|
||||
value={formData.kubernetesApps}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.kubernetesApps ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -286,6 +262,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.config.loading,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,16 +5,21 @@ import classes from './CustomQueries.module.css';
|
||||
|
||||
import Modal from '../../../UI/Modal/Modal';
|
||||
import Icon from '../../../UI/Icons/Icon/Icon';
|
||||
import { GlobalState, NewNotification, Query } from '../../../../interfaces';
|
||||
import {
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
Query,
|
||||
} from '../../../../interfaces';
|
||||
import QueriesForm from './QueriesForm';
|
||||
import { deleteQuery, createNotification } from '../../../../store/actions';
|
||||
import Button from '../../../UI/Buttons/Button/Button';
|
||||
import { searchConfig } from '../../../../utility';
|
||||
|
||||
interface Props {
|
||||
customQueries: Query[];
|
||||
deleteQuery: (prefix: string) => {};
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const CustomQueries = (props: Props): JSX.Element => {
|
||||
@@ -29,7 +34,7 @@ const CustomQueries = (props: Props): JSX.Element => {
|
||||
};
|
||||
|
||||
const deleteHandler = (query: Query) => {
|
||||
const currentProvider = searchConfig('defaultSearchProvider', 'l');
|
||||
const currentProvider = props.config.defaultSearchProvider;
|
||||
const isCurrent = currentProvider === query.prefix;
|
||||
|
||||
if (isCurrent) {
|
||||
@@ -104,6 +109,7 @@ const CustomQueries = (props: Props): JSX.Element => {
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
customQueries: state.config.customQueries,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createNotification, updateConfig } from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import {
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
Query,
|
||||
@@ -22,7 +23,7 @@ import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadli
|
||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
import { inputHandler, searchSettingsTemplate } from '../../../utility';
|
||||
|
||||
// Data
|
||||
import { queries } from '../../../utility/searchQueries.json';
|
||||
@@ -32,22 +33,17 @@ interface Props {
|
||||
updateConfig: (formData: SearchForm) => void;
|
||||
loading: boolean;
|
||||
customQueries: Query[];
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const SearchSettings = (props: Props): JSX.Element => {
|
||||
// Initial state
|
||||
const [formData, setFormData] = useState<SearchForm>({
|
||||
hideSearch: 0,
|
||||
defaultSearchProvider: 'l',
|
||||
searchSameTab: 0,
|
||||
});
|
||||
const [formData, setFormData] = useState<SearchForm>(searchSettingsTemplate);
|
||||
|
||||
// Get config
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
hideSearch: searchConfig('hideSearch', 0),
|
||||
defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'),
|
||||
searchSameTab: searchConfig('searchSameTab', 0),
|
||||
...props.config,
|
||||
});
|
||||
}, [props.loading]);
|
||||
|
||||
@@ -62,17 +58,13 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||
// Input handler
|
||||
const inputChangeHandler = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
isNumber?: boolean
|
||||
options?: { isNumber?: boolean; isBool?: boolean }
|
||||
) => {
|
||||
let value: string | number = e.target.value;
|
||||
|
||||
if (isNumber) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: value,
|
||||
inputHandler<SearchForm>({
|
||||
e,
|
||||
options,
|
||||
setStateHandler: setFormData,
|
||||
state: formData,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -110,8 +102,8 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||
<select
|
||||
id="searchSameTab"
|
||||
name="searchSameTab"
|
||||
value={formData.searchSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.searchSameTab ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -122,8 +114,8 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||
<select
|
||||
id="hideSearch"
|
||||
name="hideSearch"
|
||||
value={formData.hideSearch}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.hideSearch ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@@ -143,6 +135,7 @@ const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.config.loading,
|
||||
customQueries: state.config.customQueries,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,38 +6,40 @@ import { connect } from 'react-redux';
|
||||
import { createNotification, updateConfig } from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import { ApiResponse, GlobalState, NewNotification, Weather, WeatherForm } from '../../../interfaces';
|
||||
import {
|
||||
ApiResponse,
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
Weather,
|
||||
WeatherForm,
|
||||
} from '../../../interfaces';
|
||||
|
||||
// UI
|
||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||
import Button from '../../UI/Buttons/Button/Button';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
import { inputHandler, weatherSettingsTemplate } from '../../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
updateConfig: (formData: WeatherForm) => void;
|
||||
loading: boolean;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
||||
// Initial state
|
||||
const [formData, setFormData] = useState<WeatherForm>({
|
||||
WEATHER_API_KEY: '',
|
||||
lat: 0,
|
||||
long: 0,
|
||||
isCelsius: 1
|
||||
})
|
||||
const [formData, setFormData] = useState<WeatherForm>(
|
||||
weatherSettingsTemplate
|
||||
);
|
||||
|
||||
// Get config
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''),
|
||||
lat: searchConfig('lat', 0),
|
||||
long: searchConfig('long', 0),
|
||||
isCelsius: searchConfig('isCelsius', 1)
|
||||
})
|
||||
...props.config,
|
||||
});
|
||||
}, [props.loading]);
|
||||
|
||||
// Form handler
|
||||
@@ -48,120 +50,124 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
||||
if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
|
||||
props.createNotification({
|
||||
title: 'Warning',
|
||||
message: 'API key is missing. Weather Module will NOT work'
|
||||
})
|
||||
message: 'API key is missing. Weather Module will NOT work',
|
||||
});
|
||||
}
|
||||
|
||||
// Save settings
|
||||
await props.updateConfig(formData);
|
||||
|
||||
|
||||
// Update weather
|
||||
axios.get<ApiResponse<Weather>>('/api/weather/update')
|
||||
axios
|
||||
.get<ApiResponse<Weather>>('/api/weather/update')
|
||||
.then(() => {
|
||||
props.createNotification({
|
||||
title: 'Success',
|
||||
message: 'Weather updated'
|
||||
})
|
||||
message: 'Weather updated',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
props.createNotification({
|
||||
title: 'Error',
|
||||
message: err.response.data.error
|
||||
})
|
||||
message: err.response.data.error,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Input handler
|
||||
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => {
|
||||
let value: string | number = e.target.value;
|
||||
|
||||
if (isNumber) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: value
|
||||
})
|
||||
}
|
||||
const inputChangeHandler = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
options?: { isNumber?: boolean; isBool?: boolean }
|
||||
) => {
|
||||
inputHandler<WeatherForm>({
|
||||
e,
|
||||
options,
|
||||
setStateHandler: setFormData,
|
||||
state: formData,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => formSubmitHandler(e)}>
|
||||
<InputGroup>
|
||||
<label htmlFor='WEATHER_API_KEY'>API key</label>
|
||||
<label htmlFor="WEATHER_API_KEY">API key</label>
|
||||
<input
|
||||
type='text'
|
||||
id='WEATHER_API_KEY'
|
||||
name='WEATHER_API_KEY'
|
||||
placeholder='secret'
|
||||
type="text"
|
||||
id="WEATHER_API_KEY"
|
||||
name="WEATHER_API_KEY"
|
||||
placeholder="secret"
|
||||
value={formData.WEATHER_API_KEY}
|
||||
onChange={(e) => inputChangeHandler(e)}
|
||||
/>
|
||||
<span>
|
||||
Using
|
||||
<a
|
||||
href='https://www.weatherapi.com/pricing.aspx'
|
||||
target='blank'>
|
||||
{' '}Weather API
|
||||
<a href="https://www.weatherapi.com/pricing.aspx" target="blank">
|
||||
{' '}
|
||||
Weather API
|
||||
</a>
|
||||
. Key is required for weather module to work.
|
||||
</span>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='lat'>Location latitude</label>
|
||||
<label htmlFor="lat">Location latitude</label>
|
||||
<input
|
||||
type='number'
|
||||
id='lat'
|
||||
name='lat'
|
||||
placeholder='52.22'
|
||||
type="number"
|
||||
id="lat"
|
||||
name="lat"
|
||||
placeholder="52.22"
|
||||
value={formData.lat}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
step='any'
|
||||
lang='en-150'
|
||||
onChange={(e) => inputChangeHandler(e, { isNumber: true })}
|
||||
step="any"
|
||||
lang="en-150"
|
||||
/>
|
||||
<span>
|
||||
You can use
|
||||
<a
|
||||
href='https://www.latlong.net/convert-address-to-lat-long.html'
|
||||
target='blank'>
|
||||
{' '}latlong.net
|
||||
href="https://www.latlong.net/convert-address-to-lat-long.html"
|
||||
target="blank"
|
||||
>
|
||||
{' '}
|
||||
latlong.net
|
||||
</a>
|
||||
</span>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='long'>Location longitude</label>
|
||||
<label htmlFor="long">Location longitude</label>
|
||||
<input
|
||||
type='number'
|
||||
id='long'
|
||||
name='long'
|
||||
placeholder='21.01'
|
||||
type="number"
|
||||
id="long"
|
||||
name="long"
|
||||
placeholder="21.01"
|
||||
value={formData.long}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
step='any'
|
||||
lang='en-150'
|
||||
onChange={(e) => inputChangeHandler(e, { isNumber: true })}
|
||||
step="any"
|
||||
lang="en-150"
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='isCelsius'>Temperature unit</label>
|
||||
<label htmlFor="isCelsius">Temperature unit</label>
|
||||
<select
|
||||
id='isCelsius'
|
||||
name='isCelsius'
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.isCelsius}
|
||||
id="isCelsius"
|
||||
name="isCelsius"
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
value={formData.isCelsius ? 1 : 0}
|
||||
>
|
||||
<option value={1}>Celsius</option>
|
||||
<option value={0}>Fahrenheit</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<Button>Save changes</Button>
|
||||
<Button>Save changes</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.config.loading
|
||||
}
|
||||
}
|
||||
loading: state.config.loading,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings);
|
||||
export default connect(mapStateToProps, { createNotification, updateConfig })(
|
||||
WeatherSettings
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user