@olea-bps/components 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/AppBar/index.js +83 -0
- package/AppBar/styles.js +27 -0
- package/BookDetail/index.js +268 -0
- package/BookDetail/styles.js +124 -0
- package/Component/342/200/216JobsFilter/index.js +188 -0
- package/Component/342/200/216JobsFilter/styles.js +12 -0
- package/ConnectivityWarning/index.js +65 -0
- package/ConnectivityWarning/styles.js +19 -0
- package/ContactDetail/index.js +232 -0
- package/ContactDetail/styles.js +32 -0
- package/CourseDetail/index.js +357 -0
- package/CourseDetail/styles.js +59 -0
- package/CourseDetailDialog/index.js +169 -0
- package/CourseDetailDialog/styles.js +116 -0
- package/CourseInfo/index.js +219 -0
- package/CourseInfo/styles.js +40 -0
- package/DevelopmentDialog/index.js +208 -0
- package/DevelopmentDialog/styles.js +10 -0
- package/EventCodeInput/index.js +146 -0
- package/EventCodeInput/styles.js +108 -0
- package/FlexMenuEntry/index.js +84 -0
- package/FlexMenuEntry/styles.js +27 -0
- package/MainMenuEntry/index.js +88 -0
- package/MainMenuEntry/styles.js +28 -0
- package/MealItem/index.js +87 -0
- package/MealItem/styles.js +73 -0
- package/MensaMenu/index.js +307 -0
- package/MensaMenu/styles.js +94 -0
- package/MensaSlider/index.js +184 -0
- package/MensaSlider/styles.js +53 -0
- package/Modal/index.js +106 -0
- package/Modal/styles.js +8 -0
- package/NewsDetail/index.js +377 -0
- package/NewsDetail/styles.js +77 -0
- package/NewsList/index.js +120 -0
- package/NewsList/styles.js +19 -0
- package/NewsListItem/index.js +89 -0
- package/NewsListItem/styles.js +32 -0
- package/OtherCourses/index.js +152 -0
- package/OtherCourses/styles.js +10 -0
- package/PtsDeparture/index.js +140 -0
- package/PtsDeparture/styles.js +7 -0
- package/PtsStation/index.js +183 -0
- package/PtsStation/styles.js +47 -0
- package/QuickLinks/index.js +127 -0
- package/QuickLinks/styles.js +45 -0
- package/RoomDetail/index.js +281 -0
- package/RoomDetail/styles.js +56 -0
- package/ScaledImage/index.js +92 -0
- package/SearchResults/index.js +362 -0
- package/SearchResults/styles.js +59 -0
- package/SettingSection/index.js +54 -0
- package/SettingSection/styles.js +15 -0
- package/SettingsDialog/index.js +52 -0
- package/SettingsDialog/styles.js +12 -0
- package/SettingsDialogRadio/index.js +66 -0
- package/SettingsDialogRadio/styles.js +7 -0
- package/SettingsDialogSelect/index.js +73 -0
- package/SettingsDialogSelect/styles.js +7 -0
- package/TimetableCodeInput/index.js +201 -0
- package/TimetableCodeInput/styles.js +28 -0
- package/TimetableDay/index.js +266 -0
- package/TimetableDay/styles.js +103 -0
- package/TimetableEvent/index.js +163 -0
- package/TimetableEvent/styles.js +108 -0
- package/TimetableList/index.js +116 -0
- package/TimetableList/styles.js +109 -0
- package/TimetableMonth/index.js +156 -0
- package/TimetableMonth/styles.js +29 -0
- package/TimetableWeek/index.js +245 -0
- package/TimetableWeek/styles.js +58 -0
- package/TopNews/index.js +282 -0
- package/TopNews/styles.js +125 -0
- package/TopNewsHtwk/index.js +279 -0
- package/TopNewsHtwk/styles.js +142 -0
- package/WebView/index.js +108 -0
- package/WebView/styles.js +11 -0
- package/index.js +39 -0
- package/package.json +37 -0
|
@@ -0,0 +1,73 @@
|
|
|
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, { useMemo } from 'react';
|
|
16
|
+
import { StyleSheet } from 'react-native';
|
|
17
|
+
|
|
18
|
+
import { Checkbox, useTheme } from 'react-native-paper';
|
|
19
|
+
import { useTranslation } from 'react-i18next';
|
|
20
|
+
|
|
21
|
+
import { SettingsDialog } from '@olea-bps/components';
|
|
22
|
+
|
|
23
|
+
import componentStyles from './styles';
|
|
24
|
+
|
|
25
|
+
export default function SettingsDialogSelect({ title, visible, options, onDismiss, onOk, onChange, ...props }) {
|
|
26
|
+
const selectedOptions = Array.isArray(props?.selectedOptions) ? props.selectedOptions : [];
|
|
27
|
+
const { t } = useTranslation();
|
|
28
|
+
const theme = useTheme();
|
|
29
|
+
|
|
30
|
+
const styles = useMemo(
|
|
31
|
+
() => StyleSheet.create(componentStyles(theme)),
|
|
32
|
+
[theme, componentStyles]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<SettingsDialog
|
|
37
|
+
title={title}
|
|
38
|
+
visible={visible}
|
|
39
|
+
onDismiss={() => onDismiss(selectedOptions)}
|
|
40
|
+
onOk={() => onOk(selectedOptions)}
|
|
41
|
+
>
|
|
42
|
+
{
|
|
43
|
+
options.map(
|
|
44
|
+
option => {
|
|
45
|
+
const optionKey = option?.key;
|
|
46
|
+
const isChecked = selectedOptions?.includes?.(optionKey) ?? false;
|
|
47
|
+
const colors = theme.colors;
|
|
48
|
+
return (
|
|
49
|
+
<Checkbox.Item
|
|
50
|
+
key={optionKey}
|
|
51
|
+
value={optionKey}
|
|
52
|
+
status={isChecked ? 'checked' : 'unchecked'}
|
|
53
|
+
label={t(option.label)}
|
|
54
|
+
labelStyle={styles.optionLabel}
|
|
55
|
+
color={colors.checkboxChecked}
|
|
56
|
+
uncheckedColor={colors.checkboxUnchecked}
|
|
57
|
+
mode={'android'}
|
|
58
|
+
onPress={
|
|
59
|
+
() => {
|
|
60
|
+
const nextSelectedOptions = selectedOptions.includes(optionKey)
|
|
61
|
+
? selectedOptions.filter(selectedOption => selectedOption !== optionKey)
|
|
62
|
+
: [...selectedOptions, optionKey];
|
|
63
|
+
onChange(nextSelectedOptions);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
</SettingsDialog>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
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, { useState, useMemo } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
StyleSheet,
|
|
18
|
+
View,
|
|
19
|
+
ScrollView,
|
|
20
|
+
ActivityIndicator,
|
|
21
|
+
KeyboardAvoidingView,
|
|
22
|
+
Alert,
|
|
23
|
+
Text,
|
|
24
|
+
Linking,
|
|
25
|
+
} from 'react-native';
|
|
26
|
+
import { connect } from 'react-redux'
|
|
27
|
+
import { Paragraph, TextInput, Button, withTheme } from 'react-native-paper';
|
|
28
|
+
import { withTranslation } from 'react-i18next';
|
|
29
|
+
|
|
30
|
+
import { ApiProviderNotInitializedError, useCourses, useTimetableCode } from '@olea-bps/context-timetable';
|
|
31
|
+
|
|
32
|
+
import componentStyles from './styles';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Diese Callback-Funktion wird gerufen, wenn der vom Nutzer ausgelöste Import erfolgreich war.
|
|
36
|
+
* @callback onImportSuccessfullyFinished
|
|
37
|
+
* @returns {void}
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Diese Callback-Funktion wird gerufen, wenn der vom Nutzer ausgelöste Importversuch fehlschägt war.
|
|
42
|
+
* Es wird der Grund des Fehlschlags zur weiteren Behandlung übergeben.
|
|
43
|
+
* @callback onImportFailed
|
|
44
|
+
* @param {*} reason
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {*} props
|
|
49
|
+
* @param {onImportSuccessfullyFinished} props.onImportFailed
|
|
50
|
+
* @param {onImportFailed} props.ponImportSuccessfullyFinished
|
|
51
|
+
*/
|
|
52
|
+
function TimetableCodeInput(props) {
|
|
53
|
+
const {
|
|
54
|
+
theme,
|
|
55
|
+
theme: { colors, appSettings: { modules: { timetable: { code } } } },
|
|
56
|
+
t,
|
|
57
|
+
// Callback, welche nach dem erfolgreichen Importieren aufgerufen wird.
|
|
58
|
+
onImportSuccessfullyFinished,
|
|
59
|
+
// Callback, welche nach einem fehlgeschlagenen Importversuch aufgerufen wird.
|
|
60
|
+
onImportFailed,
|
|
61
|
+
} = props;
|
|
62
|
+
|
|
63
|
+
const componentName = TimetableCodeInput.name;
|
|
64
|
+
|
|
65
|
+
// Einstellungen für den Stundenplan-Code
|
|
66
|
+
const timetableCodeInputLength = code?.length ?? 0;
|
|
67
|
+
const timetableCodeInputFilters = code?.filters;
|
|
68
|
+
const timetableCodeInputPreSaveFilters = Array.isArray(code?.preSaveFilters) ? code.preSaveFilters : [];
|
|
69
|
+
const timetableCodeInputFilterToUpperCase = timetableCodeInputFilters?.toUpperCase ?? false;
|
|
70
|
+
|
|
71
|
+
const [timetableCode, saveTimetableCode] = useTimetableCode();
|
|
72
|
+
const [, refreshCourses] = useCourses();
|
|
73
|
+
|
|
74
|
+
const [timetableCodeInput, setTimetableCodeInput] = useState(timetableCode);
|
|
75
|
+
const [importing, setImporting] = useState(false);
|
|
76
|
+
|
|
77
|
+
// Generate styles. Will be generated only if not present or the theme property changes.
|
|
78
|
+
const styles = useMemo(
|
|
79
|
+
() => StyleSheet.create(componentStyles(theme)),
|
|
80
|
+
[theme]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const handleLinkPress = (url) => {
|
|
84
|
+
Linking.openURL(url);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const renderTextWithLinks = (text) => {
|
|
88
|
+
const parts = text.split(/(https?:\/\/[^\s]+)/g);
|
|
89
|
+
return parts.map((part, index) => {
|
|
90
|
+
if (part.match(/https?:\/\/[^\s]+/)) {
|
|
91
|
+
const displayText = part.replace(/https?:\/\//, '');
|
|
92
|
+
return (
|
|
93
|
+
<Text
|
|
94
|
+
key={index}
|
|
95
|
+
style={styles.link}
|
|
96
|
+
onPress={() => handleLinkPress(part)}
|
|
97
|
+
>
|
|
98
|
+
{displayText}
|
|
99
|
+
</Text>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return part;
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const paragraphText = t(timetableCode ? 'timetable:inputTimetableCode' : 'timetable:notImportedYet', { timetableCodeInputLength });
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<ScrollView style={styles.mainContentContainer}>
|
|
110
|
+
<KeyboardAvoidingView behavior={"padding"} >
|
|
111
|
+
<View style={styles.paragraph}>
|
|
112
|
+
{/*selectable and dataDetectorType only work on andorid*/}
|
|
113
|
+
<Paragraph style={styles.paragraphText} selectable={true} dataDetectorType={'link'}>
|
|
114
|
+
{renderTextWithLinks(paragraphText)}
|
|
115
|
+
</Paragraph>
|
|
116
|
+
</View>
|
|
117
|
+
<View style={styles.importTextInputContainer}>
|
|
118
|
+
<TextInput
|
|
119
|
+
theme={{ colors: { primary: colors.textInput, placeholder: colors.textLighter } }}
|
|
120
|
+
style={styles.paragraphText}
|
|
121
|
+
selectionColor={colors.textInputSelection}
|
|
122
|
+
label={t('timetable:inputPlaceholder')}
|
|
123
|
+
value={timetableCodeInput}
|
|
124
|
+
autoCapitalize={timetableCodeInputFilterToUpperCase ? 'characters' : 'none'}
|
|
125
|
+
maxLength={timetableCodeInputLength ? timetableCodeInputLength : null}
|
|
126
|
+
onChangeText={setTimetableCodeInput}
|
|
127
|
+
/>
|
|
128
|
+
</View>
|
|
129
|
+
<Button
|
|
130
|
+
onPress={async () => {
|
|
131
|
+
// Wenn eine Mindestlänge des Stundenplandcodes konfiguriert ist und diese nicht von der Eingabe erreicht wird, wird ein Fehlerdialog angezeigt
|
|
132
|
+
if (timetableCodeInputLength && (timetableCodeInput?.length ?? 0) < timetableCodeInputLength) {
|
|
133
|
+
const alertTranslationValues = { timetableCodeInputLength };
|
|
134
|
+
Alert.alert(
|
|
135
|
+
t('timetable:errorCodeShort:title', alertTranslationValues),
|
|
136
|
+
t('timetable:errorCodeShort:message', alertTranslationValues),
|
|
137
|
+
[{ text: 'OK' }],
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
// Import-Ladeanimation aktivieren
|
|
141
|
+
setImporting(true);
|
|
142
|
+
await saveTimetableCode(
|
|
143
|
+
// Falls Filter eingestellt sind, werden diese nacheinander auf den Stundenplancode angewendet
|
|
144
|
+
timetableCodeInputPreSaveFilters.reduce(
|
|
145
|
+
(currentTimetableCode, preSaveFilter) => preSaveFilter?.(currentTimetableCode) ?? currentTimetableCode,
|
|
146
|
+
timetableCodeInput
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
/**
|
|
150
|
+
* Dieser Aufruf ist eigentlich unnötig, aber die Nutzer möchten Feedback, wenn der Stundenplan nochmal importiert wird.
|
|
151
|
+
* Der Stundenplan wird über einen useEffect-Hook neu geladen, sobald der neue Stundenplancode vom Context gespeichert wurde.
|
|
152
|
+
* Es soll aber die Lade-Animation angezeigt werden und zum Stundenplan zurückgespungen werden, auch wenn schon ein Stundenplancode importiert wurde.
|
|
153
|
+
*
|
|
154
|
+
* Da ich mir nicht besser zu helfen weiß, wird die refreshCourses-Funktion gerufen und importiert NOCHMAL den Stundenplan, nachdem dieser schon vom useEffect-Hook im Kontext importiert wurde.
|
|
155
|
+
* refreshCourses gibt eine Promise zurück, die mit dem fertigen Import auf nachfolgende then-Anweisungen weiterleitet.
|
|
156
|
+
*
|
|
157
|
+
* Man könnte vielleicht auch bei saveTimetableCode ein Promise zurückgeben, die den Stundenplan auch gleich importiert.
|
|
158
|
+
* Aber dann müsste man anstatt des useEffekt-Hooks, der auf den Stundenplancode reagiert, jeden refresh des Stundenplanes über den Code selbst implementieren.
|
|
159
|
+
*/
|
|
160
|
+
refreshCourses()
|
|
161
|
+
// Rufe onImportSuccessfullyFinished-Callback, wenn Import erfolgreich war
|
|
162
|
+
.then(() => onImportSuccessfullyFinished?.())
|
|
163
|
+
.catch((reason) => {
|
|
164
|
+
if (reason instanceof ApiProviderNotInitializedError) {
|
|
165
|
+
console.error(componentName, ': ', reason);
|
|
166
|
+
} else {
|
|
167
|
+
console.debug(componentName, ': ', reason);
|
|
168
|
+
}
|
|
169
|
+
// Rufe onImportFailed.Callback, wenn Import fehlschlug
|
|
170
|
+
onImportFailed?.(reason);
|
|
171
|
+
})
|
|
172
|
+
// Import-Ladeanimation immer deaktivieren
|
|
173
|
+
.finally(() => setImporting(false));
|
|
174
|
+
}
|
|
175
|
+
}}
|
|
176
|
+
labelStyle={styles.importButtonLabel}
|
|
177
|
+
color={colors.buttonText}
|
|
178
|
+
>
|
|
179
|
+
{t('timetable:importButton')}
|
|
180
|
+
</Button>
|
|
181
|
+
{
|
|
182
|
+
// Es wird ein Ladesymbol angezeigt, wenn der Stundenplan importiert wird.
|
|
183
|
+
importing
|
|
184
|
+
? <>
|
|
185
|
+
<ActivityIndicator style={styles.activity} size="large" color={colors.loadingIndicator} />
|
|
186
|
+
<View style={styles.paragraph}><Paragraph>{t('timetable:importInProgress', '')}</Paragraph></View>
|
|
187
|
+
</>
|
|
188
|
+
: null
|
|
189
|
+
}
|
|
190
|
+
</KeyboardAvoidingView>
|
|
191
|
+
</ScrollView>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const mapStateToProps = state => {
|
|
196
|
+
return {
|
|
197
|
+
settings: state.settingReducer
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export default connect(mapStateToProps, null)(withTranslation()(withTheme(TimetableCodeInput)))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default function (theme) {
|
|
2
|
+
return {
|
|
3
|
+
mainContentContainer: {
|
|
4
|
+
flex: 1,
|
|
5
|
+
backgroundColor: theme.colors.contentBackground,
|
|
6
|
+
},
|
|
7
|
+
paragraph: {
|
|
8
|
+
paddingTop: theme.paddings.default,
|
|
9
|
+
paddingLeft: theme.paddings.default,
|
|
10
|
+
paddingRight: theme.paddings.default
|
|
11
|
+
},
|
|
12
|
+
paragraphText: {
|
|
13
|
+
fontSize: theme.fontSizes.l,
|
|
14
|
+
lineHeight: theme.lineHeights.l
|
|
15
|
+
},
|
|
16
|
+
importButtonLabel: {
|
|
17
|
+
fontSize: theme.fontSizes.l,
|
|
18
|
+
lineHeight: theme.lineHeights.l
|
|
19
|
+
},
|
|
20
|
+
importTextInputContainer: {
|
|
21
|
+
paddingLeft: theme.paddings.default,
|
|
22
|
+
paddingRight: theme.paddings.default
|
|
23
|
+
},
|
|
24
|
+
link: {
|
|
25
|
+
textDecorationLine: 'underline',
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
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, { useState, useMemo, useCallback, useEffect } from 'react';
|
|
16
|
+
import { View, Text, TouchableOpacity, StyleSheet, useWindowDimensions } from 'react-native';
|
|
17
|
+
import { connect } from 'react-redux';
|
|
18
|
+
import { withTheme } from 'react-native-paper';
|
|
19
|
+
import { withTranslation } from 'react-i18next';
|
|
20
|
+
import { onUpdateRefreshing } from '@olea-bps/core';
|
|
21
|
+
import componentStyles from "./styles";
|
|
22
|
+
import { Calendar } from 'react-native-big-calendar';
|
|
23
|
+
import { useCourses } from '@olea-bps/context-timetable';
|
|
24
|
+
import CalendarStrip from 'react-native-calendar-strip';
|
|
25
|
+
import CourseDetailDialog from '../component-course-detail-dialog';
|
|
26
|
+
import { DateTime, Duration } from 'luxon';
|
|
27
|
+
import { TabView } from 'react-native-tab-view';
|
|
28
|
+
import moment from 'moment';
|
|
29
|
+
import 'moment/locale/de';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Die Zeitspanne, welche vor und nach heute angezeigt werden soll.
|
|
33
|
+
*/
|
|
34
|
+
const daysTabDuration = Duration.fromISO('P6M');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
* @param {object} props
|
|
39
|
+
* @param {(DateTime) => void} props.onDateChanged Callback, welche aufgerufen wird, wenn der Tag in der Ansicht wechselt
|
|
40
|
+
* @returns
|
|
41
|
+
*/
|
|
42
|
+
function CalendarDay({ selectedDate, theme, settings, calendarScrollOffsetMinutes, onDateChanged, onCourseSelected }) {
|
|
43
|
+
const { appSettings: { modules: { timetable: { showDetails } } } } = theme;
|
|
44
|
+
const today = DateTime.now().toISODate();
|
|
45
|
+
|
|
46
|
+
const { width } = useWindowDimensions();
|
|
47
|
+
|
|
48
|
+
const [courses] = useCourses();
|
|
49
|
+
|
|
50
|
+
const language = settings?.settingsGeneral?.language;
|
|
51
|
+
|
|
52
|
+
const styles = useMemo(
|
|
53
|
+
() => StyleSheet.create(componentStyles(theme)),
|
|
54
|
+
[theme]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const calendarStripMinDate = useMemo(
|
|
58
|
+
() => DateTime.fromISO(today).minus(daysTabDuration).toJSDate(),
|
|
59
|
+
[today, daysTabDuration]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const calendarStripMaxDate = useMemo(
|
|
63
|
+
() => DateTime.fromISO(today).plus(daysTabDuration).toJSDate(),
|
|
64
|
+
[today, daysTabDuration]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const daysTabViewRoutes = useMemo(
|
|
68
|
+
() => {
|
|
69
|
+
const dateRoutes = [];
|
|
70
|
+
const todayDateTime = DateTime.fromISO(today);
|
|
71
|
+
const beginDateTime = todayDateTime.minus(daysTabDuration);
|
|
72
|
+
const endDateTime = todayDateTime.plus(daysTabDuration);
|
|
73
|
+
let currentDateTime = DateTime.fromObject(beginDateTime.toObject());
|
|
74
|
+
|
|
75
|
+
const eventColor = theme.colors.eventContainerSidebar;
|
|
76
|
+
|
|
77
|
+
while (currentDateTime <= endDateTime) {
|
|
78
|
+
const currentISODate = currentDateTime.toISODate();
|
|
79
|
+
const currentDateTimeCourses = courses?.[currentISODate]
|
|
80
|
+
?.map(
|
|
81
|
+
course => (
|
|
82
|
+
{
|
|
83
|
+
title: course.title.data,
|
|
84
|
+
start: course.startDateTime,
|
|
85
|
+
end: course.endDateTime,
|
|
86
|
+
color: eventColor,
|
|
87
|
+
type: course.type?.data,
|
|
88
|
+
professor: course.lecturer[0]?.data,
|
|
89
|
+
room: course.room.data,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
) ?? [];
|
|
93
|
+
|
|
94
|
+
dateRoutes.push(
|
|
95
|
+
{
|
|
96
|
+
key: currentISODate,
|
|
97
|
+
date: currentDateTime,
|
|
98
|
+
events: currentDateTimeCourses,
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
currentDateTime = currentDateTime.plus({ day: 1 });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return dateRoutes;
|
|
106
|
+
},
|
|
107
|
+
[today, courses, language]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const daysTabViewIndexes = useMemo(
|
|
111
|
+
() => daysTabViewRoutes
|
|
112
|
+
.reduce(
|
|
113
|
+
(accumulator, currentRoute, currentIndex) =>
|
|
114
|
+
(
|
|
115
|
+
{
|
|
116
|
+
...accumulator,
|
|
117
|
+
[currentRoute.key]: currentIndex
|
|
118
|
+
}
|
|
119
|
+
),
|
|
120
|
+
{}
|
|
121
|
+
),
|
|
122
|
+
[daysTabViewRoutes]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const daysTabViewIndex = useMemo(
|
|
126
|
+
() => {
|
|
127
|
+
const selectedISODate = selectedDate.toISODate();
|
|
128
|
+
return daysTabViewIndexes[selectedISODate];
|
|
129
|
+
},
|
|
130
|
+
[daysTabViewIndexes, selectedDate]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const renderEvent = useCallback((event, touchableOpacityProps) => {
|
|
134
|
+
const { style, ...restTouchableOpacityProps } = touchableOpacityProps;
|
|
135
|
+
return (
|
|
136
|
+
<TouchableOpacity {...restTouchableOpacityProps} style={[style, styles.eventContainer, { borderLeftColor: event.color || '#000' }]} >
|
|
137
|
+
<View style={styles.eventHeader}>
|
|
138
|
+
<Text style={styles.eventTitle} numberOfLines={1} ellipsizeMode="tail">
|
|
139
|
+
{event.title}
|
|
140
|
+
</Text>
|
|
141
|
+
<Text style={styles.eventType} numberOfLines={1} ellipsizeMode="tail">
|
|
142
|
+
{event.type}
|
|
143
|
+
</Text>
|
|
144
|
+
</View>
|
|
145
|
+
<Text style={styles.eventProfessor} numberOfLines={1} ellipsizeMode="head">
|
|
146
|
+
{event.professor}
|
|
147
|
+
</Text>
|
|
148
|
+
<View style={styles.eventFooter}>
|
|
149
|
+
<Text style={styles.eventTime} numberOfLines={1} ellipsizeMode="tail">
|
|
150
|
+
{`${event.start.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} - ${event.end.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`}
|
|
151
|
+
</Text>
|
|
152
|
+
<Text style={styles.eventRoom} numberOfLines={1} ellipsizeMode="tail">
|
|
153
|
+
{event.room}
|
|
154
|
+
</Text>
|
|
155
|
+
</View>
|
|
156
|
+
</TouchableOpacity>
|
|
157
|
+
);
|
|
158
|
+
}, [styles]);
|
|
159
|
+
|
|
160
|
+
const renderScene = useCallback(
|
|
161
|
+
({ route }) => {
|
|
162
|
+
return (
|
|
163
|
+
<View style={{ flex: 1, width: width }}>
|
|
164
|
+
<Calendar
|
|
165
|
+
events={route.events}
|
|
166
|
+
renderEvent={renderEvent}
|
|
167
|
+
renderHeader={() => null}
|
|
168
|
+
overlapOffset={95}
|
|
169
|
+
mode={'day'}
|
|
170
|
+
height={800}
|
|
171
|
+
headerContentStyle={{ backgroundColor: 'transparent' }}
|
|
172
|
+
weekStartsOn={1}
|
|
173
|
+
date={route.key}
|
|
174
|
+
swipeEnabled={false}
|
|
175
|
+
onPressEvent={
|
|
176
|
+
showDetails
|
|
177
|
+
? (event) => {
|
|
178
|
+
const eventDateISO = DateTime.fromJSDate(event.start).toISODate();
|
|
179
|
+
|
|
180
|
+
const eventStartTime = event.start.getTime();
|
|
181
|
+
const originalCourse = courses[eventDateISO]
|
|
182
|
+
?.find(
|
|
183
|
+
course =>
|
|
184
|
+
eventStartTime === course.startDateTime.getTime()
|
|
185
|
+
&&
|
|
186
|
+
event?.title === course?.title?.data
|
|
187
|
+
&&
|
|
188
|
+
event?.professor === course?.lecturer[0]?.data
|
|
189
|
+
&&
|
|
190
|
+
event?.room === course?.room?.data
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
onCourseSelected(originalCourse);
|
|
194
|
+
}
|
|
195
|
+
: null
|
|
196
|
+
}
|
|
197
|
+
scrollOffsetMinutes={calendarScrollOffsetMinutes}
|
|
198
|
+
hourStyle={styles.hourStyle}
|
|
199
|
+
/>
|
|
200
|
+
</View>
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
[styles, calendarScrollOffsetMinutes, courses, width, renderEvent]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<View style={styles.stripShadow}>
|
|
209
|
+
<CalendarStrip
|
|
210
|
+
style={styles.strip}
|
|
211
|
+
locale={{
|
|
212
|
+
name: language,
|
|
213
|
+
config: moment.localeData(language)
|
|
214
|
+
}}
|
|
215
|
+
selectedDate={selectedDate.toJSDate()}
|
|
216
|
+
onDateSelected={
|
|
217
|
+
(date) =>
|
|
218
|
+
onDateChanged?.(
|
|
219
|
+
DateTime.fromObject(
|
|
220
|
+
{
|
|
221
|
+
year: date.year(),
|
|
222
|
+
month: date.month() + 1,
|
|
223
|
+
day: date.date(),
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
highlightDateContainerStyle={styles.highlightDateContainer}
|
|
229
|
+
showMonth={false}
|
|
230
|
+
iconStyle={{ display: 'none' }}
|
|
231
|
+
scrollable={true}
|
|
232
|
+
minDate={calendarStripMinDate}
|
|
233
|
+
maxDate={calendarStripMaxDate}
|
|
234
|
+
/>
|
|
235
|
+
</View>
|
|
236
|
+
|
|
237
|
+
<TabView
|
|
238
|
+
navigationState={{ index: daysTabViewIndex, routes: daysTabViewRoutes }}
|
|
239
|
+
renderTabBar={() => null}
|
|
240
|
+
lazy
|
|
241
|
+
lazyPreloadDistance={1}
|
|
242
|
+
onIndexChange={
|
|
243
|
+
(index) => {
|
|
244
|
+
const currentDateTime = daysTabViewRoutes?.[daysTabViewIndex].date;
|
|
245
|
+
const indexDelta = index - daysTabViewIndex;
|
|
246
|
+
const deltaDateTime = currentDateTime.plus({ days: indexDelta });
|
|
247
|
+
onDateChanged?.(deltaDateTime);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
renderScene={renderScene}
|
|
251
|
+
/>
|
|
252
|
+
</>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const mapStateToProps = (state) => {
|
|
257
|
+
return {
|
|
258
|
+
pluginComponent: state.pluginReducer.timetable.component,
|
|
259
|
+
pluginStyles: state.pluginReducer.timetable.styles,
|
|
260
|
+
settings: state.settingReducer,
|
|
261
|
+
refreshing: state.stateReducer.refreshing,
|
|
262
|
+
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export default connect(mapStateToProps, { onUpdateRefreshing })(withTranslation()(withTheme(CalendarDay)));
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export default function (theme) {
|
|
2
|
+
return {
|
|
3
|
+
eventContainer: {
|
|
4
|
+
borderLeftWidth: 4,
|
|
5
|
+
backgroundColor: theme.colors.eventContainerBackground,
|
|
6
|
+
padding: 10,
|
|
7
|
+
marginBottom: 10,
|
|
8
|
+
marginRight: 10,
|
|
9
|
+
borderRadius: 8,
|
|
10
|
+
elevation: 5,
|
|
11
|
+
shadowColor: '#000',
|
|
12
|
+
shadowOffset: { width: 0, height: 2 },
|
|
13
|
+
shadowOpacity: 0.3,
|
|
14
|
+
shadowRadius: 3,
|
|
15
|
+
overflow: 'visible',
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
eventHeader: {
|
|
19
|
+
flexDirection: 'row',
|
|
20
|
+
justifyContent: 'space-between',
|
|
21
|
+
marginBottom: 0,
|
|
22
|
+
},
|
|
23
|
+
eventTitle: {
|
|
24
|
+
color: theme.colors.text,
|
|
25
|
+
fontSize: 14,
|
|
26
|
+
fontWeight: 'bold',
|
|
27
|
+
flex: 1,
|
|
28
|
+
paddingRight: 10,
|
|
29
|
+
},
|
|
30
|
+
eventType: {
|
|
31
|
+
color: theme.colors.text,
|
|
32
|
+
fontSize: 13,
|
|
33
|
+
},
|
|
34
|
+
eventProfessor: {
|
|
35
|
+
color: theme.colors.text,
|
|
36
|
+
fontSize: 13,
|
|
37
|
+
paddingTop: 4,
|
|
38
|
+
},
|
|
39
|
+
eventFooter: {
|
|
40
|
+
flexDirection: 'row',
|
|
41
|
+
justifyContent: 'space-between',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
marginTop: 4,
|
|
44
|
+
},
|
|
45
|
+
eventTime: {
|
|
46
|
+
fontSize: 13,
|
|
47
|
+
color: theme.colors.text,
|
|
48
|
+
flex: 1,
|
|
49
|
+
},
|
|
50
|
+
eventRoom: {
|
|
51
|
+
fontSize: 13,
|
|
52
|
+
color: theme.colors.text,
|
|
53
|
+
textAlign: 'right',
|
|
54
|
+
flex: 1,
|
|
55
|
+
},
|
|
56
|
+
highlightDateContainer: {
|
|
57
|
+
borderRadius: 100,
|
|
58
|
+
borderWidth: 3,
|
|
59
|
+
borderColor: 'red',
|
|
60
|
+
height: 48,
|
|
61
|
+
width: 48,
|
|
62
|
+
},
|
|
63
|
+
courseCard: {
|
|
64
|
+
paddingLeft: 5,
|
|
65
|
+
},
|
|
66
|
+
courseTimeContainerBigFont: {
|
|
67
|
+
width: "25%",
|
|
68
|
+
},
|
|
69
|
+
timeText: {
|
|
70
|
+
fontSize: theme.fontSizes.subtitle,
|
|
71
|
+
paddingTop: theme.paddings.small,
|
|
72
|
+
paddingBottom: theme.paddings.small
|
|
73
|
+
},
|
|
74
|
+
timeTextBig: {
|
|
75
|
+
fontSize: theme.fontSizes.m
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
courseContainerBigFont: {
|
|
79
|
+
width: "75%",
|
|
80
|
+
},
|
|
81
|
+
type: {
|
|
82
|
+
fontSize: theme.fontSizes.subtitle,
|
|
83
|
+
color: theme.colors.subtitle
|
|
84
|
+
},
|
|
85
|
+
strip: {
|
|
86
|
+
height: 65,
|
|
87
|
+
paddingTop: 5,
|
|
88
|
+
paddingBottom: 5,
|
|
89
|
+
elevation: 5,
|
|
90
|
+
backgroundColor: '#fff',
|
|
91
|
+
},
|
|
92
|
+
stripShadow: {
|
|
93
|
+
shadowColor: '#000',
|
|
94
|
+
shadowOffset: { width: 0, height: 3 },
|
|
95
|
+
shadowOpacity: 0.2,
|
|
96
|
+
shadowRadius: 2,
|
|
97
|
+
},
|
|
98
|
+
hourStyle: {
|
|
99
|
+
fontSize: 12,
|
|
100
|
+
color: theme.colors.text,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|