diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 87b06467..bb2e1ffe 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,11 +8,11 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 - name: Install modules run: npm install - name: Run ESLint diff --git a/android/app/build.gradle b/android/app/build.gradle index a6ce7f1f..a5fbcdc0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { applicationId 'app.neuland' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 83 - versionName "0.8.1" + versionCode 84 + versionName "0.8.2" } signingConfigs { debug { diff --git a/app.config.ts b/app.config.ts index abbc164d..a5f4ec84 100644 --- a/app.config.ts +++ b/app.config.ts @@ -27,6 +27,7 @@ module.exports = { CFBundleAllowMixedLocalizations: true, CFBundleLocalizations: ['en', 'de'], CFBundleDevelopmentRegion: 'en', + UIViewControllerBasedStatusBarAppearance: true, }, splash: { image: './src/assets/splash/splashLight.png', @@ -42,7 +43,7 @@ module.exports = { android: { package: 'app.neuland', userInterfaceStyle: 'automatic', - versionCode: 83, + versionCode: 84, splash: { image: './src/assets/splash/splashLight.png', resizeMode: 'contain', diff --git a/bun.lockb b/bun.lockb index 54544f20..51819133 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/ios/NeulandNext.xcodeproj/project.pbxproj b/ios/NeulandNext.xcodeproj/project.pbxproj index c73bba5a..e3b01f10 100644 --- a/ios/NeulandNext.xcodeproj/project.pbxproj +++ b/ios/NeulandNext.xcodeproj/project.pbxproj @@ -163,6 +163,8 @@ 416B75F9501048ACA40DB650 /* Remove signature files (Xcode 15 workaround) */, EF820F10D3E648A28C78DECD /* Remove signature files (Xcode 15 workaround) */, 1515044FA4184334AD7857BB /* Remove signature files (Xcode 15 workaround) */, + 8CFEE477FDEE49A683B8FAB4 /* Remove signature files (Xcode 15 workaround) */, + C9F03030C79E4397B5A02662 /* Remove signature files (Xcode 15 workaround) */, ); buildRules = ( ); @@ -336,6 +338,20 @@ shellPath = /bin/sh; shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; }; + 8CFEE477FDEE49A683B8FAB4 /* Remove signature files (Xcode 15 workaround) */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Remove signature files (Xcode 15 workaround)"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then\n echo \"Remove signature files (Xcode 15 workaround)\";\n rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";\n fi"; + }; AD913B7B949DB5BB3812201A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -468,6 +484,23 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeulandNext/Pods-NeulandNext-resources.sh\"\n"; showEnvVarsInLog = 0; }; + C9F03030C79E4397B5A02662 /* Remove signature files (Xcode 15 workaround) */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + name = "Remove signature files (Xcode 15 workaround)"; + inputPaths = ( + ); + outputPaths = ( + ); + shellPath = /bin/sh; + shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then + echo \"Remove signature files (Xcode 15 workaround)\"; + rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; + fi"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -522,7 +555,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.8.1; + MARKETING_VERSION = 0.8.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -530,7 +563,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "de.neuland-ingolstadt.neuland-app"; - PRODUCT_NAME = NeulandNext; + PRODUCT_NAME = "NeulandNext"; SWIFT_OBJC_BRIDGING_HEADER = "NeulandNext/NeulandNext-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -551,7 +584,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = FSXB76X6V2; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64"; INFOPLIST_FILE = NeulandNext/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; IPHONEOS_DEPLOYMENT_TARGET = 13.4; @@ -559,7 +592,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.8.1; + MARKETING_VERSION = 0.8.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -567,7 +600,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = "de.neuland-ingolstadt.neuland-app"; - PRODUCT_NAME = NeulandNext; + PRODUCT_NAME = "NeulandNext"; SWIFT_OBJC_BRIDGING_HEADER = "NeulandNext/NeulandNext-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -634,7 +667,10 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; @@ -692,7 +728,10 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/ios/NeulandNext/Info.plist b/ios/NeulandNext/Info.plist index a9c15975..d3736483 100644 --- a/ios/NeulandNext/Info.plist +++ b/ios/NeulandNext/Info.plist @@ -2,6 +2,11 @@ + Allow $(PRODUCT_NAME) to access your location + + en + de + CADisableMinimumFrameDurationOnPhone CFBundleAllowMixedLocalizations @@ -76,7 +81,7 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 - Allow $(PRODUCT_NAME) to access your location + CFBundleLocalizations en de @@ -86,7 +91,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 0.8.1 + 0.8.2 CFBundleSignature ???? CFBundleURLTypes @@ -135,6 +140,8 @@ $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route RCTAsyncStorageExcludeFromBackup @@ -167,6 +174,6 @@ UIUserInterfaceStyle Automatic UIViewControllerBasedStatusBarAppearance - + \ No newline at end of file diff --git a/ios/NeulandNext/de.lproj/Info.plist b/ios/NeulandNext/de.lproj/Info.plist index 0b96670d..c1c756bb 100644 --- a/ios/NeulandNext/de.lproj/Info.plist +++ b/ios/NeulandNext/de.lproj/Info.plist @@ -86,7 +86,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 0.8.1 + 0.8.2 CFBundleSignature ???? CFBundleURLTypes @@ -167,6 +167,6 @@ UIUserInterfaceStyle Automatic UIViewControllerBasedStatusBarAppearance - + diff --git a/ios/NeulandNext/en.lproj/Info.plist b/ios/NeulandNext/en.lproj/Info.plist index 8d8d2839..d16592ac 100644 --- a/ios/NeulandNext/en.lproj/Info.plist +++ b/ios/NeulandNext/en.lproj/Info.plist @@ -86,7 +86,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 0.8.1 + 0.8.2 CFBundleSignature ???? CFBundleURLTypes @@ -167,6 +167,6 @@ UIUserInterfaceStyle Automatic UIViewControllerBasedStatusBarAppearance - + diff --git a/ios/TestFlight/WhatToTest.de_DE.txt b/ios/TestFlight/WhatToTest.de_DE.txt index 6c3fbb45..17b05ac8 100644 --- a/ios/TestFlight/WhatToTest.de_DE.txt +++ b/ios/TestFlight/WhatToTest.de_DE.txt @@ -1,8 +1,3 @@ -Was ist neu? -- neues Karten-Design mit Darkmode -- Vorschlag für die nächste Vorlesung auf der Karte -- Speiseplan der Mensa Neuburg -- mehr Details zu CL-Veranstaltungen -- neues Standard-App-Icon -- neues Easter Egg zum Freischalten des Retro-App-Icons -- Fehlerbehebungen und Verbesserungen \ No newline at end of file +- Wähle das App Design unabhängig von deinem iOS Design (hell oder dunkel) +- Fehlerbehebungen und Verbesserungen der Karte +- Performance Verbesserungen \ No newline at end of file diff --git a/ios/TestFlight/WhatToTest.en-US.txt b/ios/TestFlight/WhatToTest.en-US.txt index 4c5b553b..71a100bf 100644 --- a/ios/TestFlight/WhatToTest.en-US.txt +++ b/ios/TestFlight/WhatToTest.en-US.txt @@ -1,8 +1,3 @@ -What's new? -- new map design with darkmode support -- next lecture suggestion on map -- Neuburg canteen meal plan -- more CL event details -- new default app icon -- new easter egg to unlock retro app icon -- bug fixes and improvements \ No newline at end of file +- choose the app design independently of your iOS design (light or dark) +- bug fixes and improvements to the map +- performance improvements \ No newline at end of file diff --git a/package.json b/package.json index 8e3c4e61..4a6ee4aa 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.1", + "version": "0.8.2", "name": "neuland", "main": "expo-router/entry", "homepage": "https://github.com/neuland-ingolstadt/neuland.app-native", diff --git a/src/app/(flow)/onboarding.tsx b/src/app/(flow)/onboarding.tsx index 5ca606a7..12da5471 100644 --- a/src/app/(flow)/onboarding.tsx +++ b/src/app/(flow)/onboarding.tsx @@ -25,6 +25,7 @@ export default function OnboardingScreen(): JSX.Element { // @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. ?.goToPage(3, false) } + controlStatusBar={false} showDone={false} nextLabel={t('onboarding.navigation.next')} skipLabel={t('onboarding.navigation.skip')} diff --git a/src/app/(flow)/whatsnew.tsx b/src/app/(flow)/whatsnew.tsx index c3c7c429..1343dd40 100644 --- a/src/app/(flow)/whatsnew.tsx +++ b/src/app/(flow)/whatsnew.tsx @@ -5,10 +5,9 @@ import changelogData from '@/data/changelog.json' import { type LanguageKey } from '@/localization/i18n' import { type Changelog } from '@/types/data' import { convertToMajorMinorPatch } from '@/utils/app-utils' -import { getContrastColor, getStatusBarStyle } from '@/utils/ui-utils' +import { getContrastColor } from '@/utils/ui-utils' import { useTheme } from '@react-navigation/native' import { router } from 'expo-router' -import { StatusBar } from 'expo-status-bar' import React from 'react' import { useTranslation } from 'react-i18next' import { Pressable, StyleSheet, Text, View } from 'react-native' @@ -23,7 +22,6 @@ export default function OnboardingScreen(): JSX.Element { const { analyticsAllowed, toggleAnalytics } = React.useContext(FlowContext) return ( - - - - { showActionSheet().catch((error) => { console.error(error) @@ -427,7 +403,7 @@ export default function TimetableDetails(): JSX.Element { {notification.mins + ' min'} )} - + */} @@ -567,16 +543,16 @@ export const styles = StyleSheet.create({ position: 'absolute', transform: [{ translateX: -1000 }], }, - bellPressable: { - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'space-around', - minWidth: 40, - height: '100%', - }, - bellTime: { - fontSize: 12, - }, + // bellPressable: { + // flexDirection: 'column', + // alignItems: 'center', + // justifyContent: 'space-around', + // minWidth: 40, + // height: '100%', + // }, + // bellTime: { + // fontSize: 12, + // }, dateRow: { flex: 1, flexDirection: 'row', diff --git a/src/app/(timetable)/webView.tsx b/src/app/(timetable)/webView.tsx index 36a8cded..8e1b2dff 100644 --- a/src/app/(timetable)/webView.tsx +++ b/src/app/(timetable)/webView.tsx @@ -1,8 +1,6 @@ import { type Colors } from '@/components/colors' -import { getStatusBarStyle } from '@/utils/ui-utils' import { useTheme } from '@react-navigation/native' import { useLocalSearchParams, useNavigation } from 'expo-router' -import { StatusBar } from 'expo-status-bar' import React, { useLayoutEffect } from 'react' import { useTranslation } from 'react-i18next' import { Dimensions, Platform, StyleSheet, View } from 'react-native' @@ -54,7 +52,6 @@ export default function NotesDetails(): JSX.Element { return ( <> - { void setDelayedIsLoaded() diff --git a/src/app/(user)/appicon.tsx b/src/app/(user)/appicon.tsx index d7dfe6c2..1a538c8a 100644 --- a/src/app/(user)/appicon.tsx +++ b/src/app/(user)/appicon.tsx @@ -4,10 +4,7 @@ import SectionView from '@/components/Elements/Universal/SectionsView' import { type Colors } from '@/components/colors' import { AppIconContext } from '@/components/contexts' import { capitalizeFirstLetter } from '@/utils/app-utils' -import { getStatusBarStyle } from '@/utils/ui-utils' import { useTheme } from '@react-navigation/native' -import { useLocalSearchParams } from 'expo-router' -import { StatusBar } from 'expo-status-bar' import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { @@ -42,7 +39,6 @@ export default function AppIconPicker(): JSX.Element { const { appIcon, toggleAppIcon, unlockedAppIcons } = useContext(AppIconContext) const { t } = useTranslation(['settings']) - const isModal = useLocalSearchParams().fromAppShortcut === 'true' const categories: Record = { exclusive: ['cat', 'retro'], default: ['default', 'modernDark', 'modernGreen'], @@ -60,7 +56,6 @@ export default function AppIconPicker(): JSX.Element { return ( <> - {Object.entries(categories).map(([key, value]) => { return ( diff --git a/src/app/(user)/license.tsx b/src/app/(user)/license.tsx index eea06a64..78fabb66 100644 --- a/src/app/(user)/license.tsx +++ b/src/app/(user)/license.tsx @@ -4,10 +4,8 @@ import SectionView from '@/components/Elements/Universal/SectionsView' import { type Colors } from '@/components/colors' import { type FormListSections } from '@/types/components' import { MODAL_BOTTOM_MARGIN, PAGE_PADDING } from '@/utils/style-utils' -import { getStatusBarStyle } from '@/utils/ui-utils' import { useTheme } from '@react-navigation/native' import { useGlobalSearchParams } from 'expo-router' -import { StatusBar } from 'expo-status-bar' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Linking, ScrollView, StyleSheet, Text, View } from 'react-native' @@ -85,7 +83,6 @@ export default function License(): JSX.Element { return ( <> - diff --git a/src/app/(user)/login.tsx b/src/app/(user)/login.tsx index d2687e35..58c32bef 100644 --- a/src/app/(user)/login.tsx +++ b/src/app/(user)/login.tsx @@ -1,8 +1,6 @@ import LoginForm from '@/components/Elements/Universal/LoginForm' import { type Colors } from '@/components/colors' -import { getStatusBarStyle } from '@/utils/ui-utils' import { useTheme } from '@react-navigation/native' -import { StatusBar } from 'expo-status-bar' import React, { useEffect, useState } from 'react' import { Dimensions, @@ -41,7 +39,6 @@ export default function Login(): JSX.Element { return ( <> - diff --git a/src/app/(user)/settings.tsx b/src/app/(user)/settings.tsx index 27fbe1b6..65023a0b 100644 --- a/src/app/(user)/settings.tsx +++ b/src/app/(user)/settings.tsx @@ -120,16 +120,6 @@ export default function Settings(): JSX.Element { { header: t('menu.formlist.preferences.title'), items: [ - { - title: t('menu.formlist.preferences.theme'), - icon: { - ios: 'paintpalette', - android: 'palette', - }, - onPress: () => { - router.push('(user)/theme') - }, - }, { title: 'Dashboard', icon: { @@ -172,6 +162,36 @@ export default function Settings(): JSX.Element { }, ], }, + { + header: t('menu.formlist.appearance.title'), + items: [ + { + title: t('menu.formlist.appearance.theme'), + icon: { + ios: 'paintpalette', + android: 'palette', + }, + onPress: () => { + router.push('(user)/theme') + }, + }, + ...(Platform.OS === 'ios' + ? [ + { + title: 'App Icon', + icon: { + ios: 'star.square.on.square', + android: null, + }, + onPress: () => { + router.push('(user)/appicon') + }, + }, + ] + : []), + ], + }, + { header: 'Quick Links', items: [ diff --git a/src/app/(user)/theme.tsx b/src/app/(user)/theme.tsx index d5526feb..c0f12dde 100644 --- a/src/app/(user)/theme.tsx +++ b/src/app/(user)/theme.tsx @@ -1,16 +1,14 @@ +import MultiSectionRadio from '@/components/Elements/Food/FoodLanguageSection' import PlatformIcon from '@/components/Elements/Universal/Icon' import SectionView from '@/components/Elements/Universal/SectionsView' import { type Colors, accentColors } from '@/components/colors' -import { AppIconContext, ThemeContext } from '@/components/contexts' +import { ThemeContext } from '@/components/contexts' import { getContrastColor } from '@/utils/ui-utils' import { useTheme } from '@react-navigation/native' import * as Haptics from 'expo-haptics' -import { router } from 'expo-router' import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { - Image, - type ImageProps, Platform, Pressable, ScrollView, @@ -19,26 +17,11 @@ import { View, } from 'react-native' -let iconImages: Record = {} - -if (Platform.OS === 'ios') { - iconImages = { - default: require('@/assets/appIcons/default.png'), - modernDark: require('@/assets/appIcons/modernDark.png'), - retro: require('@/assets/appIcons/retro.png'), - modernGreen: require('@/assets/appIcons/modernGreen.png'), - rainbowDark: require('@/assets/appIcons/rainbowDark.png'), - rainbowNeon: require('@/assets/appIcons/rainbowNeon.png'), - rainbowMoonLight: require('@/assets/appIcons/rainbowMoonLight.png'), - cat: require('@/assets/appIcons/cat.png'), - } -} - export default function Theme(): JSX.Element { const colors = useTheme().colors as Colors const deviceTheme = useTheme() - const { accentColor, toggleAccentColor } = useContext(ThemeContext) - const { appIcon } = useContext(AppIconContext) + const { accentColor, toggleAccentColor, theme, toggleTheme } = + useContext(ThemeContext) const { t } = useTranslation(['settings']) interface ColorBoxColor { @@ -137,6 +120,21 @@ export default function Theme(): JSX.Element { })) ) + const elements = [ + { + key: 'auto', + title: t('theme.themes.default'), + }, + { + key: 'light', + title: t('theme.themes.light'), + }, + { + key: 'dark', + title: t('theme.themes.dark'), + }, + ] + return ( <> @@ -157,52 +155,13 @@ export default function Theme(): JSX.Element { ))} - {Platform.OS === 'ios' && ( - - { - router.push('(user)/appicon') - }} - > - - - - {/* @ts-expect-error cannot verify that appIcon is a valid key */} - {t(`appIcon.names.${appIcon}`)} - - - - - - )} + + void} + /> + ) @@ -231,26 +190,6 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', paddingVertical: 18, }, - iconText: { - fontSize: 18, - alignSelf: 'center', - }, - iconPressable: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingStart: 12, - paddingEnd: 18, - paddingVertical: 12, - }, - iconInnerContainer: { flexDirection: 'row', gap: 20 }, - iconContainer: { - width: 80, - height: 80, - justifyContent: 'center', - borderRadius: 18, - borderWidth: 1, - }, colorBoxContainer: { justifyContent: 'center', }, diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index db1161c7..29734142 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -1,13 +1,16 @@ import PlatformIcon from '@/components/Elements/Universal/Icon' +import { ThemeContext } from '@/components/contexts' import Provider from '@/components/provider' import i18n from '@/localization/i18n' +import { getStatusBarStyle } from '@/utils/ui-utils' import AsyncStorage from '@react-native-async-storage/async-storage' +import { useTheme } from '@react-navigation/native' import * as Sentry from '@sentry/react-native' import { getLocales } from 'expo-localization' import { Stack, useNavigationContainerRef, useRouter } from 'expo-router' -import React, { useEffect } from 'react' +import React, { useContext, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { AppState, Platform, Pressable, useColorScheme } from 'react-native' +import { AppState, Platform, Pressable } from 'react-native' const sentryDsn = process.env.EXPO_PUBLIC_SENTRY_DSN const routingInstrumentation = new Sentry.ReactNavigationInstrumentation() @@ -29,8 +32,7 @@ Sentry.init({ function RootLayout(): JSX.Element { const router = useRouter() - const theme = useColorScheme() - const colorText = theme === 'dark' ? 'white' : 'black' // Use the theme value instead of dark + const { theme: appTheme } = useContext(ThemeContext) const { t } = useTranslation('navigation') const ref = useNavigationContainerRef() @@ -81,364 +83,380 @@ function RootLayout(): JSX.Element { } }, []) + const isDark = useTheme().dark + return ( <> - - - - - - - - - - - - - - - ({ - title: 'App Icon', - animation: 'slide_from_right', - presentation: - route.params?.fromAppShortcut === 'true' - ? 'modal' - : undefined, - })} - /> - - - - - - - - - - - - - - ( - { - router.push('(pages)/libraryCode') + + + + + + + + + + + + + + ({ + title: 'App Icon', + animation: 'slide_from_right', + presentation: + route.params?.fromAppShortcut === 'true' + ? 'modal' + : undefined, + })} + /> + + + + + + + + + + + + + + ( + { + router.push('(pages)/libraryCode') + }} + > + - - - ), - }} - /> - - - - - - - + android={{ + name: 'barcode_scanner', + size: 24, + }} + /> + + ), + }} + /> + + + + + + ) } -export default Sentry.wrap(RootLayout) +const ProviderComponent = (): JSX.Element => { + return ( + + + + ) +} + +export default Sentry.wrap(ProviderComponent) diff --git a/src/components/Elements/Food/HeaderRight.tsx b/src/components/Elements/Food/HeaderRight.tsx index 7eb8bc47..61cfa1fd 100644 --- a/src/components/Elements/Food/HeaderRight.tsx +++ b/src/components/Elements/Food/HeaderRight.tsx @@ -1,5 +1,4 @@ -import { type Colors } from '@/components/colors' -import { type Theme, useTheme } from '@react-navigation/native' +import { useTheme } from '@react-navigation/native' import { router } from 'expo-router' import React from 'react' import { Pressable, StyleSheet, View } from 'react-native' @@ -7,8 +6,7 @@ import { Pressable, StyleSheet, View } from 'react-native' import PlatformIcon from '../Universal/Icon' export const FoodHeaderRight = (): JSX.Element => { - const theme: Theme = useTheme() - const colors = theme.colors as Colors + const isDark = useTheme().dark return ( { > ('') let filteredEntries = Object.entries(data) @@ -56,11 +64,11 @@ const ItemsPickerScreen = (params: { placeholder: t(`navigation.${placeholderKey}`, { ns: 'navigation', }), - + textColor: colors.text, + barTintColor: getBarTintColor(theme, isDark), ...Platform.select({ android: { headerIconColor: colors.text, - textColor: colors.text, hintTextColor: colors.text, }, }), @@ -71,11 +79,10 @@ const ItemsPickerScreen = (params: { }, }, }) - }, [navigation, colors.text]) + }, [navigation, colors.text, isDark]) return ( - - {t('empty.' + type)} + {t( + // @ts-expect-error Translation key is dynamic + 'empty.' + type + )} )} diff --git a/src/components/Elements/Map/BottomSheetBackground.tsx b/src/components/Elements/Map/BottomSheetBackground.tsx index 4d3f8d86..4d9dd356 100644 --- a/src/components/Elements/Map/BottomSheetBackground.tsx +++ b/src/components/Elements/Map/BottomSheetBackground.tsx @@ -16,7 +16,11 @@ const BottomSheetBackground = (): JSX.Element => { }, ]} > - + ) : ( { + const colors = useTheme().colors as Colors + const { t } = useTranslation('common') + + return ( + + { + void Linking.openURL( + 'https://www.openstreetmap.org/copyright' + ) + }} + style={styles.attributionLink} + > + + {t('pages.map.details.osm')} + + + + + ) +} + const MapBottomSheet: React.FC = ({ bottomSheetRef, currentPosition, @@ -118,7 +156,7 @@ const MapBottomSheet: React.FC = ({ addUnlockedAppIcon('retro') } }, [localSearch]) - + const textInputRef = useRef(null) return ( = ({ backgroundComponent={BottomSheetBackground} animatedPosition={currentPosition} keyboardBehavior="extend" + onChange={(index) => { + if (index <= 1) { + localSearch !== '' && setLocalSearch('') + textInputRef.current?.blur() + } + }} > = ({ > {Platform.OS === 'ios' ? ( = ({ onFocus={() => { bottomSheetRef.current?.snapToIndex(2) }} - onEndEditing={() => { - bottomSheetRef.current?.collapse() - }} /> ) : ( = ({ }} /> )} + {localSearch !== '' ? ( searchResultsExact.length > 0 || searchResultsFuzzy.length > 0 ? ( @@ -224,6 +268,9 @@ const MapBottomSheet: React.FC = ({ bottomSheetRef={bottomSheetRef} /> )} + ItemSeparatorComponent={() => ( + + )} stickySectionHeadersEnabled={false} renderSectionHeader={({ section: { title }, @@ -249,14 +296,17 @@ const MapBottomSheet: React.FC = ({ ) ) : userKind === USER_GUEST ? ( - - {t('pages.map.details.room.signIn')} - + + + {t('pages.map.details.room.signIn')} + + + ) : ( <> {nextLecture !== null && nextLecture.length > 0 && ( @@ -623,39 +673,10 @@ const MapBottomSheet: React.FC = ({ )} + )} - - { - void Linking.openURL( - 'https://www.openstreetmap.org/copyright' - ) - }} - style={styles.attributionLink} - > - - {t('pages.map.details.osm')} - - - - ) @@ -760,6 +781,10 @@ const styles = StyleSheet.create({ }, attributionText: { fontSize: 15, - paddingStart: 12, + paddingStart: 4, + }, + guestContainer: { + paddingTop: 15, + gap: 35, }, }) diff --git a/src/components/Elements/Map/MapScreen.tsx b/src/components/Elements/Map/MapScreen.tsx index dc0fb9f6..0544ce60 100644 --- a/src/components/Elements/Map/MapScreen.tsx +++ b/src/components/Elements/Map/MapScreen.tsx @@ -36,7 +36,7 @@ import { type BottomSheetModal } from '@gorhom/bottom-sheet' import MapLibreGL from '@maplibre/maplibre-react-native' import { useTheme } from '@react-navigation/native' import { useQuery } from '@tanstack/react-query' -import { useFocusEffect, useNavigation } from 'expo-router' +import { useNavigation } from 'expo-router' import { type Feature, type FeatureCollection, @@ -47,6 +47,7 @@ import React, { useCallback, useContext, useEffect, + useLayoutEffect, useMemo, useRef, useState, @@ -54,11 +55,11 @@ import React, { import { useTranslation } from 'react-i18next' import { ActivityIndicator, + Appearance, LayoutAnimation, Linking, Platform, Pressable, - StatusBar, StyleSheet, Text, View, @@ -119,8 +120,17 @@ const MapScreen = (): JSX.Element => { const locations: LocationsType = Locations const [isVisible, setIsVisible] = useState(true) const opacity = useSharedValue(1) + + // needed for Android void MapLibreGL.setAccessToken(null) + useLayoutEffect(() => { + navigation.setOptions({ + statusbarTranslucent: true, + statusBarColor: undefined, + }) + }, [navigation]) + const toggleShowAllFloors = (): void => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) setShowAllFloors(!showAllFloors) @@ -133,6 +143,7 @@ const MapScreen = (): JSX.Element => { return { transform: [{ translateY: bottom }], + height: opacity.value === 0 ? 0 : 'auto', opacity: opacity.value, } }) @@ -221,6 +232,16 @@ const MapScreen = (): JSX.Element => { return nextEvent != null ? [nextEvent] : [] } + useEffect(() => { + const subscription = Appearance.addChangeListener(() => { + bottomSheetModalRef.current?.close() + }) + + return () => { + subscription.remove() + } + }) + useEffect(() => { if (timetable == null) { return @@ -592,13 +613,6 @@ const MapScreen = (): JSX.Element => { } }, [clickedElement]) - useFocusEffect(() => { - StatusBar.setBarStyle(theme.dark ? 'light-content' : 'dark-content') - return () => { - StatusBar.setBarStyle('default') - } - }) - useEffect(() => { setDisableFollowUser(false) }, [cameraTriggerKey]) @@ -616,7 +630,7 @@ const MapScreen = (): JSX.Element => { fillOpacity: 0.1, }, allRoomsOutline: { - lineColor: isDark ? '#2d3035' : '#979797', + lineColor: isDark ? '#2d3035' : '#8e8e8e', lineWidth: 2.3, }, availableRooms: { @@ -624,12 +638,12 @@ const MapScreen = (): JSX.Element => { fillOpacity: 0.2, }, availableRoomsOutline: { - lineWidth: 2.3, + lineWidth: 2.4, }, osmBackground: { backgroundColor: isDark - ? 'rgba(166, 173, 181, 0.69)' - : 'rgba(222, 221, 203, 0.69)', + ? 'rgba(166, 173, 181, 0.70)' + : 'rgba(218, 218, 218, 0.70)', paddingHorizontal: 4, borderRadius: 4, }, @@ -716,6 +730,14 @@ const MapScreen = (): JSX.Element => { onRegionIsChanging={() => { setRegionChange(true) }} + compassViewMargins={ + Platform.OS === 'android' + ? { + x: 5, + y: 70, + } + : undefined + } > { clickedElement == null && !disableFollowUser } + triggerKey={cameraTriggerKey} followUserMode={MapLibreGL.UserTrackingMode.Follow} /> { const { setClickedElement, setLocalSearch, setCurrentFloor } = useContext(MapContext) - console.log(result) return ( - - { - const center = result.item.properties?.center - Keyboard.dismiss() - bottomSheetRef.current?.collapse() - // _setView(center, mapRef) - setClickedElement({ - data: result.title, - type: result.item.properties?.rtype, - center, - manual: false, - }) - setCurrentFloor({ - floor: - (result.item.properties?.Ebene as string) ?? 'EG', - manual: false, - }) - handlePresentModalPress() - // _injectMarker(mapRef, center, colors) - setLocalSearch('') + { + const center = result.item.properties?.center + Keyboard.dismiss() + bottomSheetRef.current?.collapse() + // _setView(center, mapRef) + setClickedElement({ + data: result.title, + type: result.item.properties?.rtype, + center, + manual: false, + }) + setCurrentFloor({ + floor: (result.item.properties?.Ebene as string) ?? 'EG', + manual: false, + }) + handlePresentModalPress() + // _injectMarker(mapRef, center, colors) + setLocalSearch('') + }} + > + - + + + + - - - - - - {result.title} - - - {result.subtitle} - - - - {index !== 9 && } - + {result.title} + + + {result.subtitle} + + + ) } diff --git a/src/components/Elements/Timetable/HeaderButtons.tsx b/src/components/Elements/Timetable/HeaderButtons.tsx index 8659e292..a6ab66b1 100644 --- a/src/components/Elements/Timetable/HeaderButtons.tsx +++ b/src/components/Elements/Timetable/HeaderButtons.tsx @@ -1,4 +1,3 @@ -import { type Colors } from '@/components/colors' import { TimetableContext } from '@/components/contexts' import { trackEvent } from '@aptabase/react-native' import { useTheme } from '@react-navigation/native' @@ -8,7 +7,7 @@ import { Pressable, StyleSheet } from 'react-native' import PlatformIcon from '../Universal/Icon' export function HeaderLeft(): JSX.Element { - const colors = useTheme().colors as Colors + const isDark = useTheme().dark const { timetableMode, setTimetableMode } = useContext(TimetableContext) return ( @@ -24,7 +23,7 @@ export function HeaderLeft(): JSX.Element { style={styles.headerButton} > - - - - - - + + + + + ) } diff --git a/src/components/contexts.ts b/src/components/contexts.ts index b011c330..4c1f61b1 100644 --- a/src/components/contexts.ts +++ b/src/components/contexts.ts @@ -42,6 +42,8 @@ export const UserKindContext = createContext({ }) export const ThemeContext = createContext({ + theme: 'auto', + toggleTheme: () => {}, accentColor: 'blue', toggleAccentColor: () => {}, }) diff --git a/src/components/provider.tsx b/src/components/provider.tsx index 4804860a..b2780c88 100644 --- a/src/components/provider.tsx +++ b/src/components/provider.tsx @@ -12,6 +12,7 @@ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persi import { QueryClient, focusManager } from '@tanstack/react-query' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import { usePathname } from 'expo-router' +import 'expo-status-bar' import React, { useEffect } from 'react' import { type AppStateStatus, @@ -144,6 +145,15 @@ export default function Provider({ }) }, [themeHook.accentColor, flow.analyticsInitialized]) + useEffect(() => { + if (!flow.analyticsInitialized) { + return + } + trackEvent('Theme', { + theme: themeHook.theme, + }) + }, [themeHook.accentColor, flow.analyticsInitialized]) + useEffect(() => { if (!flow.analyticsInitialized) { return @@ -236,15 +246,20 @@ export default function Provider({ }) }, [flow.analyticsAllowed, flow.analyticsInitialized]) + const getTheme = (): AppTheme => { + if (themeHook.theme === 'auto') { + return colorScheme === 'dark' ? darkTheme : lightTheme + } + return themeHook.theme === 'dark' ? darkTheme : lightTheme + } + return ( - + diff --git a/src/hooks/contexts/theme.ts b/src/hooks/contexts/theme.ts index f9b38ed0..06de040d 100644 --- a/src/hooks/contexts/theme.ts +++ b/src/hooks/contexts/theme.ts @@ -3,7 +3,9 @@ import { useEffect, useState } from 'react' export interface ThemeHook { accentColor: string + theme: 'light' | 'dark' | 'auto' toggleAccentColor: (name: string) => void + toggleTheme: (theme: 'light' | 'dark' | 'auto') => void } /** @@ -13,16 +15,27 @@ export interface ThemeHook { */ export function useTheme(): ThemeHook { const [accentColor, setAccentColor] = useState('blue') + const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto') useEffect(() => { const loadAsyncStorageData = async (): Promise => { try { - const data = await AsyncStorage.getItem('accentColor') - if (data != null) { - setAccentColor(data) + const keys = ['accentColor', 'theme'] + const [accentColorData, themeData] = await Promise.all( + keys.map(async (key) => await AsyncStorage.getItem(key)) + ) + + if (accentColorData != null) { + setAccentColor(accentColorData) } else { setAccentColor('blue') } + + if (themeData != null) { + setTheme(themeData as 'light' | 'dark' | 'auto') + } else { + setTheme('auto') + } } catch (error) { console.error( 'Error while retrieving theme data from AsyncStorage:', @@ -42,8 +55,19 @@ export function useTheme(): ThemeHook { void AsyncStorage.setItem('accentColor', name) } + /** + * Function to toggle the theme of the app. + * @param theme - The new theme to be set. + */ + function toggleTheme(theme: 'light' | 'dark' | 'auto'): void { + setTheme(theme) + void AsyncStorage.setItem('theme', theme) + } + return { accentColor, toggleAccentColor, + theme, + toggleTheme, } } diff --git a/src/localization/de/settings.ts b/src/localization/de/settings.ts index 18a4f66b..fbbf70e8 100644 --- a/src/localization/de/settings.ts +++ b/src/localization/de/settings.ts @@ -21,10 +21,13 @@ export default { formlist: { preferences: { title: 'Einstellungen', - theme: 'Design', food: 'Essen', language: 'Sprache', }, + appearance: { + title: 'Darstellung', + theme: 'Design', + }, language: { title: 'Sprache wechseln', message: @@ -118,10 +121,6 @@ export default { accent: { title: 'Akzentfarbe', }, - exclusive: { - title: 'Exklusive Designs', - description: 'Demnächst verfügbar', - }, colors: { teal: 'Türkis', blue: 'Blau', @@ -133,7 +132,13 @@ export default { orange: 'Orange', green: 'Neuland', }, - footer: 'Ändere die Akzentfarbe, um deinen Stil zu entsprechen. Das ändert die Farbe der Symbole und Schaltflächen in der App.', + footer: 'Das ändert die Farbe der Symbole und Schaltflächen in der App.', + themes: { + title: 'Design', + default: 'Automatisch', + dark: 'Dunkel', + light: 'Hell', + }, }, dashboard: { shown: 'Angezeigte Karten', diff --git a/src/localization/en/navigation.ts b/src/localization/en/navigation.ts index 73fb8fce..dc65a0f9 100644 --- a/src/localization/en/navigation.ts +++ b/src/localization/en/navigation.ts @@ -11,7 +11,7 @@ export default { allergensSearch: 'Search allergens', allergens: 'Allergens', details: 'Details', - theme: 'Appearance', + theme: 'Design', profile: 'Profile', about: 'About', advancedSearch: 'Advanced Search', diff --git a/src/localization/en/settings.ts b/src/localization/en/settings.ts index 62fe1524..dab4db01 100644 --- a/src/localization/en/settings.ts +++ b/src/localization/en/settings.ts @@ -19,10 +19,13 @@ export default { formlist: { preferences: { title: 'Preferences', - theme: 'Appearance', food: 'Food', language: 'Language', }, + appearance: { + title: 'Appearance', + theme: 'Design', + }, language: { title: 'Change language', message: 'Confirm to change the app language to German.', @@ -116,10 +119,6 @@ export default { accent: { title: 'Accent color', }, - exclusive: { - title: 'Exclusive designs', - description: 'Coming soon', - }, colors: { teal: 'Teal', blue: 'Blue', @@ -131,7 +130,13 @@ export default { orange: 'Orange', green: 'Neuland', }, - footer: 'Change the accent color to match your style. This changes the color of the icons and buttons in the app.', + footer: 'This changes the color of the icons and buttons in the app.', + themes: { + title: 'Theme', + default: 'Automatic', + dark: 'Dark', + light: 'Light', + }, }, dashboard: { shown: 'Shown cards', diff --git a/src/utils/ui-utils.ts b/src/utils/ui-utils.ts index 19a69159..8d8bd3ad 100644 --- a/src/utils/ui-utils.ts +++ b/src/utils/ui-utils.ts @@ -110,17 +110,6 @@ export const lighten = (percentage: number, color: string): string => { return newColor } -/** - * Returns the appropriate status bar style based on the platform. Used for Status Bar component in modal screens. - * @returns The appropriate status bar style. - * @example - * // Usage - * - */ -export const getStatusBarStyle = (): 'light' | 'auto' => { - return Platform.OS === 'ios' ? (Platform.isPad ? 'auto' : 'light') : 'auto' -} - let toast: any = null export const showToast = async ( message: string, @@ -140,3 +129,36 @@ export const showToast = async ( ...options, }) } + +export const getStatusBarStyle = ( + theme: 'light' | 'dark' | 'auto' +): 'light' | 'dark' | 'auto' => { + switch (theme) { + case 'light': + return 'dark' + case 'dark': + return 'light' + default: + return 'auto' + } +} + +export function getBarTintColor( + theme: string, + isDark: boolean +): string | undefined { + if (Platform.OS === 'android') { + return undefined + } + const darkDarkColor = '#2e2e2e' + const darkLightColor = '#8f8f8f' + const lightDarkColor = '#999999' + const lightLightColor = '#ebebeb' + if (theme === 'auto') { + return undefined + } + if (theme === 'dark') { + return isDark ? darkDarkColor : darkLightColor + } + return isDark ? lightDarkColor : lightLightColor +}