@olea-bps/views 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Callmanager/index.js +148 -0
- package/Callmanager/styles.js +62 -0
- package/Canteens/index.js +383 -0
- package/Canteens/styles.js +89 -0
- package/Dashboard/index.js +176 -0
- package/Dashboard/styles.js +16 -0
- package/DashboardHtwk/index.js +197 -0
- package/DashboardHtwk/styles.js +24 -0
- package/Event/index.js +369 -0
- package/Event/styles.js +63 -0
- package/FeedList/index.js +165 -0
- package/FeedList/styles.js +15 -0
- package/FeedNews/index.js +243 -0
- package/FeedNews/styles.js +32 -0
- package/Flexmenu/index.js +185 -0
- package/Flexmenu/styles.js +62 -0
- package/FlexmenuWebview/index.js +167 -0
- package/FlexmenuWebview/styles.js +16 -0
- package/Howhy/index.js +151 -0
- package/Howhy/styles.js +11 -0
- package/Jobs/index.js +217 -0
- package/Jobs/styles.js +41 -0
- package/MainMenu/index.js +329 -0
- package/MainMenu/styles.js +36 -0
- package/NewsTabbar/index.js +164 -0
- package/NewsTabbar/styles.js +27 -0
- package/Opal/index.js +136 -0
- package/Opal/styles.js +11 -0
- package/PublicTransportTicket/index.js +173 -0
- package/PublicTransportTicket/styles.js +17 -0
- package/Search/index.js +186 -0
- package/Search/styles.js +67 -0
- package/SettingsAccessibility/index.js +148 -0
- package/SettingsAccessibility/styles.js +13 -0
- package/SettingsAppInfo/index.js +86 -0
- package/SettingsAppInfo/styles.js +18 -0
- package/SettingsCanteens/index.js +249 -0
- package/SettingsCanteens/styles.js +33 -0
- package/SettingsGeneral/index.js +173 -0
- package/SettingsGeneral/styles.js +28 -0
- package/SettingsNotifications/index.js +204 -0
- package/SettingsNotifications/styles.js +38 -0
- package/SettingsUser/index.js +170 -0
- package/SettingsUser/styles.js +17 -0
- package/TimetableCalendar/index.js +322 -0
- package/TimetableCalendar/styles.js +117 -0
- package/TimetableList/index.js +397 -0
- package/TimetableList/styles.js +55 -0
- package/Webviews/index.js +158 -0
- package/Webviews/styles.js +11 -0
- package/index.js +25 -0
- package/package.json +41 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
* you may not use this file except in compliance with the License.
|
|
4
|
+
* You may obtain a copy of the License at
|
|
5
|
+
*
|
|
6
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
*
|
|
8
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
* See the License for the specific language governing permissions and
|
|
12
|
+
* limitations under the License.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import {
|
|
17
|
+
ActivityIndicator,
|
|
18
|
+
Text,
|
|
19
|
+
SafeAreaView,
|
|
20
|
+
StyleSheet,
|
|
21
|
+
View,
|
|
22
|
+
FlatList,
|
|
23
|
+
TouchableOpacity, Image, Linking
|
|
24
|
+
} from 'react-native';
|
|
25
|
+
import {connect} from 'react-redux'
|
|
26
|
+
import {withTheme} from "react-native-paper";
|
|
27
|
+
import {withTranslation} from "react-i18next";
|
|
28
|
+
|
|
29
|
+
import merge from 'lodash/merge';
|
|
30
|
+
import moment from "moment";
|
|
31
|
+
|
|
32
|
+
import {feedApi, onUpdateRefreshing} from "@olea-bps/core";
|
|
33
|
+
import { AppBar as AppbarComponent } from '@olea-bps/components';
|
|
34
|
+
import IconsOpenasist from "@olea-bps/icons-openasist";
|
|
35
|
+
|
|
36
|
+
import componentStyles from "./styles";
|
|
37
|
+
import { handleHtmlEntities } from '@olea-bps/core/helper/format.helper';
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Feed News View
|
|
42
|
+
*
|
|
43
|
+
* Shows a list of news of the provided feed
|
|
44
|
+
*
|
|
45
|
+
* Parameters:
|
|
46
|
+
* - none
|
|
47
|
+
*
|
|
48
|
+
* Navigation-Parameters:
|
|
49
|
+
* - feedId: Id of the feed to load the news from
|
|
50
|
+
* - feedTitle: Name/Title of the feed for view header
|
|
51
|
+
*/
|
|
52
|
+
class FeedNewsView extends React.Component {
|
|
53
|
+
static navigationOptions = {
|
|
54
|
+
header: null
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Styles of this component
|
|
58
|
+
styles;
|
|
59
|
+
|
|
60
|
+
feedNews = null;
|
|
61
|
+
|
|
62
|
+
constructor(props) {
|
|
63
|
+
super(props);
|
|
64
|
+
|
|
65
|
+
this.state = {
|
|
66
|
+
inProgress: true,
|
|
67
|
+
hasNoNewsAvailable: false,
|
|
68
|
+
hasError: false
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ------------------------------------------------------------------------
|
|
72
|
+
// PLUGIN FUNCTIONALITY
|
|
73
|
+
// ------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const {pluginStyles,theme} = this.props;
|
|
76
|
+
this.styles = componentStyles(theme);
|
|
77
|
+
|
|
78
|
+
if (pluginStyles) {
|
|
79
|
+
this.styles = merge(this.styles, pluginStyles);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.styles = StyleSheet.create(this.styles);
|
|
83
|
+
|
|
84
|
+
// ------------------------------------------------------------------------
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* User pressed a card to read this news
|
|
89
|
+
*
|
|
90
|
+
* @param news
|
|
91
|
+
*
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
_onCardPress(news) {
|
|
95
|
+
this.props.navigation.navigate('NewsDetail', {news: news});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render a single news item with title, description and image
|
|
101
|
+
*
|
|
102
|
+
* @param item
|
|
103
|
+
* @returns {*}
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
_renderNewsItem = ({item}) => {
|
|
107
|
+
const { theme: { themeStyles, colors}, t } = this.props;
|
|
108
|
+
|
|
109
|
+
var description = item.shortDesc
|
|
110
|
+
.replace(/<(?:.|\n)*?>/gm, '')
|
|
111
|
+
.replace('Weiterlesen ›', '');
|
|
112
|
+
|
|
113
|
+
let date = moment(item.pubdate);
|
|
114
|
+
|
|
115
|
+
let pubDate = item.pubdate;
|
|
116
|
+
if(date) {
|
|
117
|
+
pubDate = date.format('DD.MM.YYYY - HH:mm');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<TouchableOpacity style={themeStyles.cardWithImage} onPress={() => this._onCardPress(item)}>
|
|
122
|
+
<View style={themeStyles.cartHeaderSplit}>
|
|
123
|
+
{item.imageUrl && <Image source={{uri: item.imageUrl }} style={themeStyles.cartHeaderSplitImage} resizeMode="contain"/>}
|
|
124
|
+
<View style={item.imageUrl ? themeStyles.cartHeaderSplitDetails : themeStyles.cartHeaderDetails}>
|
|
125
|
+
<Text style={[themeStyles.cardTitle, this.styles.newsTitle]}>{item.title}</Text>
|
|
126
|
+
<View style={[themeStyles.flexRow, {alignItems: 'center', alignContent: 'center'}]}>
|
|
127
|
+
<IconsOpenasist icon={'time'} size={20} color={colors.accent}/>
|
|
128
|
+
<Text style={[themeStyles.cardSubTitle, this.styles.newsDate]}>{pubDate}</Text>
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
</View>
|
|
132
|
+
<View style={themeStyles.flexRow}>
|
|
133
|
+
<View style={[themeStyles.cardContent, this.styles.newsContent]}>
|
|
134
|
+
<Text style={themeStyles.cardText}>{handleHtmlEntities(description)}</Text>
|
|
135
|
+
</View>
|
|
136
|
+
<View style={[themeStyles.cardRightIcon, this.styles.newsIcon]}>
|
|
137
|
+
<IconsOpenasist icon={"forward"} size={25} color={colors.messages.icon} />
|
|
138
|
+
</View>
|
|
139
|
+
</View>
|
|
140
|
+
{item.link ? (
|
|
141
|
+
<TouchableOpacity style={themeStyles.cardLinkExternal} onPress={() => {Linking.openURL(item.link)}}>
|
|
142
|
+
<Text style={themeStyles.cardLinkTextExternal}>{t('common:openInBrowser')}</Text>
|
|
143
|
+
<IconsOpenasist icon={"open-external"} size={18} color={colors.subtitle} />
|
|
144
|
+
</TouchableOpacity>
|
|
145
|
+
) : null}
|
|
146
|
+
</TouchableOpacity>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Key Extractor for unique item key
|
|
152
|
+
*
|
|
153
|
+
* @param item
|
|
154
|
+
* @param index
|
|
155
|
+
* @returns {*}
|
|
156
|
+
* @private
|
|
157
|
+
*/
|
|
158
|
+
_keyExtractor = (item, index) => {
|
|
159
|
+
return ((item.guid) ? item.guid : ((item.title) ? item.title : index));
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
render() {
|
|
163
|
+
|
|
164
|
+
// ------------------------------------------------------------------------
|
|
165
|
+
// PLUGIN FUNCTIONALITY
|
|
166
|
+
// ------------------------------------------------------------------------
|
|
167
|
+
const PluginComponent = this.props.pluginComponent;
|
|
168
|
+
if (PluginComponent) {
|
|
169
|
+
return <PluginComponent />;
|
|
170
|
+
}
|
|
171
|
+
// ------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
const {route, t} = this.props;
|
|
175
|
+
const {colors, paddings, themeStyles} = this.props.theme;
|
|
176
|
+
const {inProgress, hasNoNewsAvailable, hasError} = this.state;
|
|
177
|
+
|
|
178
|
+
const feedId = route.params && route.params.feedId || null;
|
|
179
|
+
const feedTitle = route.params && route.params.feedTitle || 'News';
|
|
180
|
+
|
|
181
|
+
if(!this.feedNews && feedId) {
|
|
182
|
+
feedApi.getFeedById(feedId, (feed) => {
|
|
183
|
+
if(typeof feed === 'number') {
|
|
184
|
+
this.feedNews = [];
|
|
185
|
+
this.setState({
|
|
186
|
+
inProgress: false,
|
|
187
|
+
hasNoNewsAvailable: feed === 0,
|
|
188
|
+
hasError: feed === -1
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
this.feedNews = feed;
|
|
192
|
+
this.setState({
|
|
193
|
+
inProgress: false,
|
|
194
|
+
hasNoNewsAvailable: false,
|
|
195
|
+
hasError: false
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<SafeAreaView style={[this.styles.container, themeStyles.appSafeAreaContainer]}>
|
|
203
|
+
<AppbarComponent {...this.props} title={feedTitle}/>
|
|
204
|
+
<View style={themeStyles.container}>
|
|
205
|
+
{ !inProgress && hasNoNewsAvailable &&
|
|
206
|
+
<View style={themeStyles.noticeTextContainer}>
|
|
207
|
+
<Text style={themeStyles.noticeText}>{t('news:noResult')}</Text>
|
|
208
|
+
</View>
|
|
209
|
+
}
|
|
210
|
+
{ !inProgress && hasError &&
|
|
211
|
+
<View style={themeStyles.noticeTextContainer}>
|
|
212
|
+
<Text style={themeStyles.noticeText}>{t('news:couldNotLoadNews')}</Text>
|
|
213
|
+
</View>
|
|
214
|
+
}
|
|
215
|
+
{ inProgress && <ActivityIndicator style={this.styles.activity} size="large" color={colors.loadingIndicator} /> }
|
|
216
|
+
{ !inProgress && <FlatList
|
|
217
|
+
data={this.feedNews}
|
|
218
|
+
extraData={this.state}
|
|
219
|
+
keyExtractor={this._keyExtractor}
|
|
220
|
+
renderItem={this._renderNewsItem}
|
|
221
|
+
initialNumToRender={6}
|
|
222
|
+
getItemLayout={(data, index) => (
|
|
223
|
+
{length: 50, offset: 50 * index, index}
|
|
224
|
+
)}
|
|
225
|
+
contentContainerStyle={{paddingBottom: paddings.default}}
|
|
226
|
+
style={this.styles.innerContainer}
|
|
227
|
+
/> }
|
|
228
|
+
|
|
229
|
+
</View>
|
|
230
|
+
</SafeAreaView>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const mapStateToProps = state => {
|
|
236
|
+
return {
|
|
237
|
+
pluginComponent: state.pluginReducer.feedNews.component,
|
|
238
|
+
pluginStyles: state.pluginReducer.feedNews.styles,
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
export default connect(mapStateToProps, {onUpdateRefreshing})(withTranslation()(withTheme(FeedNewsView)))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export default function(theme) {
|
|
2
|
+
return {
|
|
3
|
+
container: {
|
|
4
|
+
flex: 1
|
|
5
|
+
},
|
|
6
|
+
innerContainer: {
|
|
7
|
+
flex: 1,
|
|
8
|
+
},
|
|
9
|
+
activity: {
|
|
10
|
+
margin: 40
|
|
11
|
+
},
|
|
12
|
+
newsTitle: {
|
|
13
|
+
...theme.fonts.medium,
|
|
14
|
+
fontSize: theme.fontSizes.xl,
|
|
15
|
+
lineHeight: theme.lineHeights.xxl,
|
|
16
|
+
marginBottom: theme.paddings.default
|
|
17
|
+
},
|
|
18
|
+
newsDate: {
|
|
19
|
+
...theme.fonts.bold,
|
|
20
|
+
marginTop: 3,
|
|
21
|
+
paddingLeft: theme.paddings.small
|
|
22
|
+
},
|
|
23
|
+
newsContent: {
|
|
24
|
+
paddingTop: theme.paddings.default,
|
|
25
|
+
paddingLeft: theme.paddings.default,
|
|
26
|
+
paddingRight: theme.paddings.default
|
|
27
|
+
},
|
|
28
|
+
newsIcon: {
|
|
29
|
+
paddingRight: theme.paddings.xsmall
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
* you may not use this file except in compliance with the License.
|
|
4
|
+
* You may obtain a copy of the License at
|
|
5
|
+
*
|
|
6
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
*
|
|
8
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
* See the License for the specific language governing permissions and
|
|
12
|
+
* limitations under the License.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useMemo } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
StyleSheet,
|
|
18
|
+
SafeAreaView,
|
|
19
|
+
View,
|
|
20
|
+
Text,
|
|
21
|
+
FlatList,
|
|
22
|
+
Linking
|
|
23
|
+
} from 'react-native';
|
|
24
|
+
|
|
25
|
+
import { connect } from 'react-redux';
|
|
26
|
+
import { useTheme } from 'react-native-paper';
|
|
27
|
+
import { useTranslation } from 'react-i18next';
|
|
28
|
+
import { useNavigation } from '@react-navigation/native';
|
|
29
|
+
|
|
30
|
+
import { AppBar as AppbarComponent } from '@olea-bps/components';
|
|
31
|
+
import IconsOpenasist from '@olea-bps/icons-openasist';
|
|
32
|
+
import { FlexMenuEntry } from '@olea-bps/components';
|
|
33
|
+
|
|
34
|
+
import componentStyles from './styles';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Diese View zeigt eine Ebene von Menüeintragen des Flex-Menüs an.
|
|
38
|
+
*
|
|
39
|
+
* Es sollte mindestens ein Menütyp(props.menuType) über die Eigenschaften oder ein Hauptmenüeintrag(props.route.mainMenuEntry) übergeben werden.
|
|
40
|
+
* Wird ein Menütyp übergeben, wird der Hauptmenüeintrag des Menütypes gesucht und die Kindermenüeintrage angezeigt.
|
|
41
|
+
* Wird ein Hauptmenüeintrag bzw. Elternmenüeintrag übergeben, werden dessen Kindereinträge angezeigt.
|
|
42
|
+
* Der Hauptmenüeintrag wird über das Navigationsframework übergeben, weshalb dieser in der 'route'-Eigenschaft zu finden ist.
|
|
43
|
+
* Somit werden meherer Menüebenen unterstützt.
|
|
44
|
+
*
|
|
45
|
+
* Die View heißt aus historischen Gründen CoronaView und sollte zeitnah sinvoll umbennant werden
|
|
46
|
+
*
|
|
47
|
+
* @param {object} props
|
|
48
|
+
* @param {string} [props.menuType] Menütyp, dessen erste Menüebene angezeigt werden soll
|
|
49
|
+
* @param {object} props.route Eigenschaften, welche durch das Navigationsframework übergeben werden: {@link https://reactnavigation.org/docs/params}
|
|
50
|
+
* @param {string} [props.route.mainMenuEntry] ID des Hauptmenüeintrag, dessen Kinder anzuzeigen sind
|
|
51
|
+
*/
|
|
52
|
+
function FlexMenuView(props) {
|
|
53
|
+
const componentName = FlexMenuView.name;
|
|
54
|
+
const {
|
|
55
|
+
route,
|
|
56
|
+
} = props;
|
|
57
|
+
|
|
58
|
+
const menuType = props?.menuType ?? route?.params?.menuType;
|
|
59
|
+
const menuEntries = props.menuEntries ?? [];
|
|
60
|
+
const view = props?.view ?? route.params.view;
|
|
61
|
+
|
|
62
|
+
const theme = useTheme();
|
|
63
|
+
const { themeStyles, paddings, colors, appSettings } = theme;
|
|
64
|
+
const { t, i18n } = useTranslation();
|
|
65
|
+
const navigation = useNavigation();
|
|
66
|
+
|
|
67
|
+
const languages = appSettings?.languages;
|
|
68
|
+
// Eingestellte Sprache aus der Übersetzung holen
|
|
69
|
+
const selectedLanguage = i18n.language;
|
|
70
|
+
// Hinterlegte Sprachen durchgehen und nach dem erweiterten Sprachcode holen
|
|
71
|
+
const menuEntriesLanguage = languages
|
|
72
|
+
// Finde die derzeitige eingestellte Sprache
|
|
73
|
+
.find(language => language?.code === selectedLanguage)
|
|
74
|
+
// Erweiterten Sprachcode von der gefundenen Sprache auslesen
|
|
75
|
+
?.extCode;
|
|
76
|
+
|
|
77
|
+
let mainMenuEntry;
|
|
78
|
+
let childMenuEntries;
|
|
79
|
+
|
|
80
|
+
const styles = useMemo(
|
|
81
|
+
() => StyleSheet.create(componentStyles(theme)),
|
|
82
|
+
[theme]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Bestimmen des Hauptmenüeintrages dieser View
|
|
86
|
+
// Zuerst wird der Hauptmenüeintrag es den Navigationsparametern entnommen
|
|
87
|
+
mainMenuEntry = route?.params?.mainMenuEntry ?? null;
|
|
88
|
+
// Ist der Hauptmenüeintrag nicht in den Navigationsparametern enthalten gewesen, aber dennoch ist ein Menütyp vergeben, wird der Hauptmenüeintrag aus allen Menueinträgen bestimmt
|
|
89
|
+
if (mainMenuEntry === null && menuType !== null) {
|
|
90
|
+
// Der Hauptmenüeintrag ist ein Eintrag ohne einen Elterneintrag, mit dem übergenen Menütyp und der eingestellten Sprache
|
|
91
|
+
mainMenuEntry = menuEntries
|
|
92
|
+
// Es werden alle Menüeintrage behalten, die dem Menütyp entsprechen
|
|
93
|
+
.filter(menuEntry => menuEntry.type === menuType)
|
|
94
|
+
// Es werden alle Menüeintrage behalten, die der eingestellten Sprache entsprechen
|
|
95
|
+
.filter(menuEntry => (menuEntry.language ?? null) === menuEntriesLanguage)
|
|
96
|
+
// Aus den nicht ausgefilterten Menüeinträgen wird eine Eintrag genommen, der kein Elterneintrag besitzt
|
|
97
|
+
.find(menuEntry => (menuEntry.parentMenuEntry ?? null) === null);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ids der Untermenüpunkte aus den Ressource-links herausholen
|
|
101
|
+
const childMenuEntryIds = mainMenuEntry
|
|
102
|
+
?.subMenuEntries
|
|
103
|
+
?.map(
|
|
104
|
+
subMenuLinkId =>
|
|
105
|
+
// Der Link des Untermenüpunktes wird in seine Segmente aufgesplitted und das letzte Segment mit Inhalt als ID verwendet
|
|
106
|
+
subMenuLinkId
|
|
107
|
+
// Aufsplitten der URL in seine Segmente
|
|
108
|
+
.split('/')
|
|
109
|
+
// Entfernen aller 'leeren' Segmenente
|
|
110
|
+
.filter(urlSegment => urlSegment !== '')
|
|
111
|
+
// Nehmen des letzten Elements
|
|
112
|
+
.pop()
|
|
113
|
+
)
|
|
114
|
+
?? [];
|
|
115
|
+
|
|
116
|
+
childMenuEntries = childMenuEntryIds
|
|
117
|
+
.map(childMenuEntryId => menuEntries.find(menuEntry => menuEntry.id === childMenuEntryId))
|
|
118
|
+
.filter(menuEntry => (menuEntry ?? null) !== null);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<SafeAreaView style={[styles.container, themeStyles.appSafeAreaContainer]}>
|
|
122
|
+
<AppbarComponent {...props} title={mainMenuEntry?.title} />
|
|
123
|
+
<View style={themeStyles.container}>
|
|
124
|
+
<FlatList
|
|
125
|
+
data={
|
|
126
|
+
Array.isArray(childMenuEntries)
|
|
127
|
+
? childMenuEntries
|
|
128
|
+
: []
|
|
129
|
+
}
|
|
130
|
+
renderItem={
|
|
131
|
+
({ item: menuEntry }) => {
|
|
132
|
+
let menuEntryOnPress;
|
|
133
|
+
switch (menuEntry.openIn) {
|
|
134
|
+
case 'webview':
|
|
135
|
+
menuEntryOnPress = () => { navigation.push('FlexMenuWebView', { url: menuEntry.url, item: menuEntry }) };
|
|
136
|
+
break;
|
|
137
|
+
case 'webbrowser':
|
|
138
|
+
case 'browser':
|
|
139
|
+
menuEntryOnPress = () => {
|
|
140
|
+
Linking.canOpenURL(menuEntry.url)
|
|
141
|
+
.then(supported => {
|
|
142
|
+
if (supported) {
|
|
143
|
+
Linking.openURL(menuEntry.url);
|
|
144
|
+
} else {
|
|
145
|
+
console.debug(componentName, ':', 'Can not open', menuEntry?.url);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
case null:
|
|
151
|
+
menuEntryOnPress = () => { navigation.push(view, { mainMenuEntry: menuEntry, view: view }) };
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<FlexMenuEntry
|
|
157
|
+
title={menuEntry?.title}
|
|
158
|
+
description={menuEntry?.description}
|
|
159
|
+
iconImage={menuEntry?.image}
|
|
160
|
+
onPress={menuEntryOnPress}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
keyExtractor={item => item.id}
|
|
166
|
+
contentContainerStyle={{ paddingBottom: paddings.default }}
|
|
167
|
+
ListEmptyComponent={
|
|
168
|
+
<View style={[styles.containerInner, styles.containerErrorMsg]}>
|
|
169
|
+
<IconsOpenasist icon={'info'} size={48} color={colors.primary} />
|
|
170
|
+
<Text style={styles.title}>{t('corona:menuEntriesNotAvailable')}</Text>
|
|
171
|
+
</View>
|
|
172
|
+
}
|
|
173
|
+
/>
|
|
174
|
+
</View>
|
|
175
|
+
</SafeAreaView>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const mapStateToProps = state => {
|
|
180
|
+
return {
|
|
181
|
+
menuEntries: state.apiReducer.menuEntries,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default connect(mapStateToProps)(FlexMenuView)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export default function(theme) {
|
|
2
|
+
return {
|
|
3
|
+
container: {
|
|
4
|
+
flex: 1,
|
|
5
|
+
backgroundColor: theme.colors.background,
|
|
6
|
+
},
|
|
7
|
+
contentContainer: {
|
|
8
|
+
paddingTop: 0
|
|
9
|
+
},
|
|
10
|
+
titleStyle: {
|
|
11
|
+
...theme.fonts.medium,
|
|
12
|
+
fontSize: theme.fontSizes.xxl,
|
|
13
|
+
alignSelf: "center",
|
|
14
|
+
},
|
|
15
|
+
cardContent: {
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
alignItems: 'flex-start'
|
|
18
|
+
},
|
|
19
|
+
cardTitle: {
|
|
20
|
+
flexWrap: 'wrap',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
fontSize: theme.fontSizes.l,
|
|
23
|
+
...theme.fonts.light,
|
|
24
|
+
},
|
|
25
|
+
cardImage: {
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
alignContent: 'center',
|
|
28
|
+
paddingRight: theme.paddings.default,
|
|
29
|
+
},
|
|
30
|
+
cardSubTitle: {
|
|
31
|
+
width: 250,
|
|
32
|
+
fontSize: theme.fontSizes.s,
|
|
33
|
+
...theme.fonts.regular,
|
|
34
|
+
lineHeight: theme.lineHeights.xxs,
|
|
35
|
+
color: theme.colors.subtitle,
|
|
36
|
+
marginTop: theme.paddings.xsmall
|
|
37
|
+
},
|
|
38
|
+
title : {
|
|
39
|
+
fontSize: theme.fontSizes.title,
|
|
40
|
+
color: theme.colors.text,
|
|
41
|
+
textAlign: 'center',
|
|
42
|
+
marginTop: theme.paddings.default,
|
|
43
|
+
marginLeft: theme.paddings.default,
|
|
44
|
+
marginRight: theme.paddings.default,
|
|
45
|
+
},
|
|
46
|
+
subtitle : {
|
|
47
|
+
fontSize: theme.fontSizes.subtitle,
|
|
48
|
+
color: theme.colors.subtitle,
|
|
49
|
+
textAlign: 'center',
|
|
50
|
+
marginTop: theme.paddings.default,
|
|
51
|
+
marginLeft: theme.paddings.default,
|
|
52
|
+
marginRight: theme.paddings.default,
|
|
53
|
+
},
|
|
54
|
+
containerErrorMsg: {
|
|
55
|
+
flex: 1,
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
alignContent: 'center',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
margin: theme.paddings.default,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
* you may not use this file except in compliance with the License.
|
|
4
|
+
* You may obtain a copy of the License at
|
|
5
|
+
*
|
|
6
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
*
|
|
8
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
* See the License for the specific language governing permissions and
|
|
12
|
+
* limitations under the License.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useMemo, useState, useCallback, useRef } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
StyleSheet,
|
|
18
|
+
SafeAreaView,
|
|
19
|
+
ActivityIndicator,
|
|
20
|
+
BackHandler,
|
|
21
|
+
Platform,
|
|
22
|
+
} from 'react-native';
|
|
23
|
+
|
|
24
|
+
import { Appbar, useTheme } from "react-native-paper";
|
|
25
|
+
import { useTranslation } from "react-i18next";
|
|
26
|
+
import { useFocusEffect } from '@react-navigation/native';
|
|
27
|
+
import { WebView } from 'react-native-webview';
|
|
28
|
+
|
|
29
|
+
import { AppBar as AppbarComponent } from '@olea-bps/components';
|
|
30
|
+
import IconsOpenasist from '@olea-bps/icons-openasist';
|
|
31
|
+
|
|
32
|
+
import componentStyles from "./styles";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Corona Info View
|
|
36
|
+
*
|
|
37
|
+
* The home screen of the app. Shows different informations like the top news, weather,
|
|
38
|
+
* current mensa menus and informations of the next course (if the student has imported his timtetable)
|
|
39
|
+
*
|
|
40
|
+
* Parameters:
|
|
41
|
+
* - none
|
|
42
|
+
*
|
|
43
|
+
* Navigation-Parameters:
|
|
44
|
+
* - none
|
|
45
|
+
*/
|
|
46
|
+
export default function FlexMenuWebView({ route, ...props }) {
|
|
47
|
+
const theme = useTheme();
|
|
48
|
+
const { colors, themeStyles } = theme;
|
|
49
|
+
const { t } = useTranslation();
|
|
50
|
+
const flexMenuEntry = route?.params?.item ?? props?.item;
|
|
51
|
+
|
|
52
|
+
const webviewURL = route?.params?.url ?? props?.url;
|
|
53
|
+
const webviewTitle = flexMenuEntry.title;
|
|
54
|
+
|
|
55
|
+
const webview = useRef(null);
|
|
56
|
+
const [webviewCanGoBack, setWebviewCanGoBack] = useState();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Erstellen einer Liste von erlauben Hosts, die in der Webview verwendet werden dürfen.
|
|
60
|
+
* In der Liste ist nur ein Host, welcher aus der anzuzeigenden URL extrahiert wird.
|
|
61
|
+
*
|
|
62
|
+
* Unter iOS wird der `undefined`-Wert verwendet.
|
|
63
|
+
* Wird in der Webseite eine Ressource von einem anderem Host eingebunden, öffnet es die Ressource im Browser, weil der Dritthost nicht erlaubt ist.
|
|
64
|
+
* Somit führen zum Beispiel eingebundene Youtube-Videos zu einem öffnen des Videos im Browser.
|
|
65
|
+
*
|
|
66
|
+
* @type {string[]|undefined}
|
|
67
|
+
*/
|
|
68
|
+
const webviewOriginWhitelist = useMemo(
|
|
69
|
+
() => {
|
|
70
|
+
// URL-String in URL-Instanze konvertieren
|
|
71
|
+
const url = new URL(webviewURL);
|
|
72
|
+
// Host/Origin aus URL extrahieren
|
|
73
|
+
const webviewURLOrigin = url.origin;
|
|
74
|
+
|
|
75
|
+
// Auswahl das Rückgabewerted abhängig vond der Plattform
|
|
76
|
+
// Standartmäßig wird eine Liste mit dem Host der URL zurückgegeben
|
|
77
|
+
// Unter iOS wird undefined zurückgegeben
|
|
78
|
+
return Platform.select({
|
|
79
|
+
default: [webviewURLOrigin],
|
|
80
|
+
ios: undefined,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
[webviewURL]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Styles of this component
|
|
87
|
+
const styles = useMemo(
|
|
88
|
+
() => StyleSheet.create(componentStyles(theme)),
|
|
89
|
+
[theme]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
useFocusEffect(
|
|
93
|
+
useCallback(
|
|
94
|
+
() => {
|
|
95
|
+
const onBackPress = webviewCanGoBack && webview?.current
|
|
96
|
+
? () => {
|
|
97
|
+
webview?.current?.goBack();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
: () => {
|
|
101
|
+
props?.navigation?.goBack();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const subscription = BackHandler.addEventListener(
|
|
106
|
+
'hardwareBackPress',
|
|
107
|
+
onBackPress
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return () => subscription.remove();
|
|
111
|
+
},
|
|
112
|
+
[webviewCanGoBack]
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<SafeAreaView style={[styles.container, themeStyles.appSafeAreaContainer]}>
|
|
118
|
+
<AppbarComponent {...props}
|
|
119
|
+
title={t(webviewTitle)}
|
|
120
|
+
leftAction={
|
|
121
|
+
<Appbar.Action
|
|
122
|
+
icon={props => <IconsOpenasist {...props} icon={'back'} color={colors.primaryText} />}
|
|
123
|
+
onPress={
|
|
124
|
+
webviewCanGoBack
|
|
125
|
+
? webview?.current?.goBack
|
|
126
|
+
: props?.navigation?.goBack
|
|
127
|
+
}
|
|
128
|
+
accessible={true}
|
|
129
|
+
accessibilityLabel={
|
|
130
|
+
webviewCanGoBack
|
|
131
|
+
? t('accessibility:appbar.navigateBack')
|
|
132
|
+
: t('accessibility:appbar.back')
|
|
133
|
+
}
|
|
134
|
+
/>
|
|
135
|
+
}
|
|
136
|
+
rightAction={
|
|
137
|
+
<Appbar.Action
|
|
138
|
+
icon="refresh"
|
|
139
|
+
onPress={webview?.current?.reload}
|
|
140
|
+
accessible={true}
|
|
141
|
+
accessibilityLabel={t('accessibility:appbar.reload')}
|
|
142
|
+
/>
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
<WebView
|
|
146
|
+
ref={webview}
|
|
147
|
+
source={{ uri: webviewURL }}
|
|
148
|
+
startInLoadingState={true}
|
|
149
|
+
allowsFullscreenVideo={true}
|
|
150
|
+
allowsInlineMediaPlayback={true}
|
|
151
|
+
originWhitelist={webviewOriginWhitelist}
|
|
152
|
+
renderLoading={
|
|
153
|
+
() =>
|
|
154
|
+
<ActivityIndicator
|
|
155
|
+
style={styles.activity}
|
|
156
|
+
size='large'
|
|
157
|
+
color={colors.loadingIndicator}
|
|
158
|
+
/>
|
|
159
|
+
}
|
|
160
|
+
onLoad={
|
|
161
|
+
syntheticEvent =>
|
|
162
|
+
setWebviewCanGoBack(syntheticEvent?.nativeEvent?.canGoBack ?? false)
|
|
163
|
+
}
|
|
164
|
+
/>
|
|
165
|
+
</SafeAreaView>
|
|
166
|
+
);
|
|
167
|
+
}
|