diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b59c9c47..291670d5 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,44 +6,46 @@ To contribute, please follow these guidelines: ## Getting Started -1. Fork the repository and clone it to your local machine. -2. Install the required software: +1. Fork the repository and clone it to your local machine. +2. Install the required software: - - [Visual Studio Code](https://code.visualstudio.com/) + - [Visual Studio Code](https://code.visualstudio.com/) - [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) (for iOS development, macOS only) - [Android Studio](https://developer.android.com/studio) (for Android development) -3. Install the required dependencies: +3. Install the required dependencies: - [Node.js 18 LTS](https://nodejs.org/en/) or higher - [Bun](https://bun.sh) or simply use npm or yarn - [Watchman](https://facebook.github.io/watchman/docs/install) (for Linux or macOS users) -4. In addition is recommended to use the [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) extensions for your IDE to get real-time feedback on your code. +4. In addition is recommended to use the [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) extensions for your IDE to get real-time feedback on your code. -5. Install project dependencies by running the following command in the project directory: +5. Install project dependencies by running the following command in the project directory: ```bash bun install ``` -6. Setup the emulator +6. Setup the emulator Android (_Windows, macOS, and Linux_): - - Follow the [official guide](https://docs.expo.dev/workflow/android-studio-emulator/) to set up the Android emulator. - + + - Follow the [official guide](https://docs.expo.dev/workflow/android-studio-emulator/) to set up the Android emulator. + iOS (_macOS only_): - - Follow the [official guide](https://docs.expo.dev/workflow/ios-simulator/) to set up the iOS simulator. + - Follow the [official guide](https://docs.expo.dev/workflow/ios-simulator/) to set up the iOS simulator. -5. Create a development build for your desired platform. +7. Create a development build for your desired platform. - ```bash - bun ios - bun android - ``` -> [!NOTE] -> Step 7 is required every time the dependencies are updated. + ```bash + bun ios + bun android + ``` + + > [!NOTE] + > Step 7 is required every time the dependencies are updated. ## Development @@ -51,11 +53,11 @@ To contribute, please follow these guidelines: 2. Make your changes and ensure that the code follows our coding style and conventions. 3. Run the app locally with Expo by running the following command in the project directory: - ```bash + ```bash bun start ``` - This allows you to run the app one the previously created development build. Expo Go is not supported for development. + This allows you to run the app one the previously created development build. Expo Go is not supported for development. ## Submitting Changes diff --git a/android/.gitignore b/android/.gitignore index 877b87e9..8a6be077 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -10,6 +10,7 @@ build/ local.properties *.iml *.hprof +.cxx/ # Bundle artifacts *.jsbundle diff --git a/android/app/build.gradle b/android/app/build.gradle index 7cbac98a..a6ce7f1f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() @@ -11,11 +12,11 @@ react { entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" - codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() // Use Expo CLI to bundle the app, this ensures the Metro config // works correctly with Expo projects. - cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim()) + cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) bundleCommand = "export:embed" /* Folders */ @@ -76,21 +77,21 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea */ def jscFlavor = 'org.webkit:android-jsc:+' -apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle") +apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle") + android { ndkVersion rootProject.ext.ndkVersion - compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion namespace 'app.neuland' defaultConfig { applicationId 'app.neuland' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 78 - versionName "0.7.1" - - buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString()) + versionCode 83 + versionName "0.8.1" } signingConfigs { debug { @@ -113,6 +114,11 @@ android { proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } + packagingOptions { + jniLibs { + useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) + } + } } // Apply static values from `gradle.properties` to the `android.packagingOptions` @@ -142,34 +148,21 @@ dependencies { def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; - def frescoVersion = rootProject.ext.frescoVersion - - // If your app supports Android versions before Ice Cream Sandwich (API level 14) - if (isGifEnabled || isWebpEnabled) { - implementation("com.facebook.fresco:fresco:${frescoVersion}") - implementation("com.facebook.fresco:imagepipeline-okhttp3:${frescoVersion}") - } if (isGifEnabled) { // For animated gif support - implementation("com.facebook.fresco:animated-gif:${frescoVersion}") + implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}") } if (isWebpEnabled) { // For webp support - implementation("com.facebook.fresco:webpsupport:${frescoVersion}") + implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}") if (isWebpAnimatedEnabled) { // Animated webp support - implementation("com.facebook.fresco:animated-webp:${frescoVersion}") + implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}") } } - debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") - debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { - exclude group:'com.squareup.okhttp3', module:'okhttp' - } - debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") - if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") } else { @@ -177,5 +170,5 @@ dependencies { } } -apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); +apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/android/app/src/debug/java/app/neuland/ReactNativeFlipper.java b/android/app/src/debug/java/app/neuland/ReactNativeFlipper.java deleted file mode 100644 index 6ba7abb3..00000000 --- a/android/app/src/debug/java/app/neuland/ReactNativeFlipper.java +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - *

This source code is licensed under the MIT license found in the LICENSE file in the root - * directory of this source tree. - */ -package app.neuland; - -import android.content.Context; -import com.facebook.flipper.android.AndroidFlipperClient; -import com.facebook.flipper.android.utils.FlipperUtils; -import com.facebook.flipper.core.FlipperClient; -import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; -import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; -import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; -import com.facebook.flipper.plugins.inspector.DescriptorMapping; -import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; -import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; -import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; -import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; -import com.facebook.react.ReactInstanceEventListener; -import com.facebook.react.ReactInstanceManager; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.modules.network.NetworkingModule; -import okhttp3.OkHttpClient; - -/** - * Class responsible of loading Flipper inside your React Native application. This is the debug - * flavor of it. Here you can add your own plugins and customize the Flipper setup. - */ -public class ReactNativeFlipper { - public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { - if (FlipperUtils.shouldEnableFlipper(context)) { - final FlipperClient client = AndroidFlipperClient.getInstance(context); - - client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); - client.addPlugin(new DatabasesFlipperPlugin(context)); - client.addPlugin(new SharedPreferencesFlipperPlugin(context)); - client.addPlugin(CrashReporterPlugin.getInstance()); - - NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); - NetworkingModule.setCustomClientBuilder( - new NetworkingModule.CustomClientBuilder() { - @Override - public void apply(OkHttpClient.Builder builder) { - builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); - } - }); - client.addPlugin(networkFlipperPlugin); - client.start(); - - // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized - // Hence we run if after all native modules have been initialized - ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); - if (reactContext == null) { - reactInstanceManager.addReactInstanceEventListener( - new ReactInstanceEventListener() { - @Override - public void onReactContextInitialized(ReactContext reactContext) { - reactInstanceManager.removeReactInstanceEventListener(this); - reactContext.runOnNativeModulesQueueThread( - new Runnable() { - @Override - public void run() { - client.addPlugin(new FrescoFlipperPlugin()); - } - }); - } - }); - } else { - client.addPlugin(new FrescoFlipperPlugin()); - } - } - } -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 33ead621..f986f06b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@ - @@ -18,12 +17,11 @@ - + - - + diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png index afe7d9ff..b8dfe191 100644 Binary files a/android/app/src/main/ic_launcher-playstore.png and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/java/app/neuland/MainActivity.java b/android/app/src/main/java/app/neuland/MainActivity.java deleted file mode 100644 index 2087b356..00000000 --- a/android/app/src/main/java/app/neuland/MainActivity.java +++ /dev/null @@ -1,65 +0,0 @@ -package app.neuland; - -import android.os.Build; -import android.os.Bundle; - -import com.facebook.react.ReactActivity; -import com.facebook.react.ReactActivityDelegate; -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; -import com.facebook.react.defaults.DefaultReactActivityDelegate; - -import expo.modules.ReactActivityDelegateWrapper; - -public class MainActivity extends ReactActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - // Set the theme to AppTheme BEFORE onCreate to support - // coloring the background, status bar, and navigation bar. - // This is required for expo-splash-screen. - setTheme(R.style.AppTheme); - super.onCreate(null); - } - - /** - * Returns the name of the main component registered from JavaScript. - * This is used to schedule rendering of the component. - */ - @Override - protected String getMainComponentName() { - return "main"; - } - - /** - * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link - * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React - * (aka React 18) with two boolean flags. - */ - @Override - protected ReactActivityDelegate createReactActivityDelegate() { - return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate( - this, - getMainComponentName(), - // If you opted-in for the New Architecture, we enable the Fabric Renderer. - DefaultNewArchitectureEntryPoint.getFabricEnabled())); - } - - /** - * Align the back button behavior with Android S - * where moving root activities to background instead of finishing activities. - * @see onBackPressed - */ - @Override - public void invokeDefaultOnBackPressed() { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { - if (!moveTaskToBack(false)) { - // For non-root activities, use the default implementation to finish them. - super.invokeDefaultOnBackPressed(); - } - return; - } - - // Use the default back button implementation on Android S - // because it's doing more than {@link Activity#moveTaskToBack} in fact. - super.invokeDefaultOnBackPressed(); - } -} diff --git a/android/app/src/main/java/app/neuland/MainActivity.kt b/android/app/src/main/java/app/neuland/MainActivity.kt new file mode 100644 index 00000000..22bc8766 --- /dev/null +++ b/android/app/src/main/java/app/neuland/MainActivity.kt @@ -0,0 +1,61 @@ +package app.neuland + +import android.os.Build +import android.os.Bundle + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +import expo.modules.ReactActivityDelegateWrapper + +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + setTheme(R.style.AppTheme); + super.onCreate(null) + } + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "main" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate { + return ReactActivityDelegateWrapper( + this, + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + object : DefaultReactActivityDelegate( + this, + mainComponentName, + fabricEnabled + ){}) + } + + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + * @see onBackPressed + */ + override fun invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed() + } + return + } + + // Use the default back button implementation on Android S + // because it's doing more than [Activity.moveTaskToBack] in fact. + super.invokeDefaultOnBackPressed() + } +} diff --git a/android/app/src/main/java/app/neuland/MainApplication.java b/android/app/src/main/java/app/neuland/MainApplication.java deleted file mode 100644 index c1ff74ef..00000000 --- a/android/app/src/main/java/app/neuland/MainApplication.java +++ /dev/null @@ -1,80 +0,0 @@ -package app.neuland; - -import android.app.Application; -import android.content.res.Configuration; -import androidx.annotation.NonNull; - -import com.facebook.react.PackageList; -import com.facebook.react.ReactApplication; -import com.facebook.react.ReactNativeHost; -import com.facebook.react.ReactPackage; -import com.facebook.react.config.ReactFeatureFlags; -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; -import com.facebook.react.defaults.DefaultReactNativeHost; -import com.facebook.soloader.SoLoader; - -import expo.modules.ApplicationLifecycleDispatcher; -import expo.modules.ReactNativeHostWrapper; - -import java.util.List; - -public class MainApplication extends Application implements ReactApplication { - - private final ReactNativeHost mReactNativeHost = - new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) { - @Override - public boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } - - @Override - protected List getPackages() { - @SuppressWarnings("UnnecessaryLocalVariable") - List packages = new PackageList(this).getPackages(); - // Packages that cannot be autolinked yet can be added manually here, for example: - // packages.add(new MyReactNativePackage()); - return packages; - } - - @Override - protected String getJSMainModuleName() { - return ".expo/.virtual-metro-entry"; - } - - @Override - protected boolean isNewArchEnabled() { - return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; - } - - @Override - protected Boolean isHermesEnabled() { - return BuildConfig.IS_HERMES_ENABLED; - } - }); - - @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; - } - - @Override - public void onCreate() { - super.onCreate(); - SoLoader.init(this, /* native exopackage */ false); - if (!BuildConfig.REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS) { - ReactFeatureFlags.unstable_useRuntimeSchedulerAlways = false; - } - if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. - DefaultNewArchitectureEntryPoint.load(); - } - ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); - ApplicationLifecycleDispatcher.onApplicationCreate(this); - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig); - } -} diff --git a/android/app/src/main/java/app/neuland/MainApplication.kt b/android/app/src/main/java/app/neuland/MainApplication.kt new file mode 100644 index 00000000..a905c998 --- /dev/null +++ b/android/app/src/main/java/app/neuland/MainApplication.kt @@ -0,0 +1,55 @@ +package app.neuland + +import android.app.Application +import android.content.res.Configuration + +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.ReactHost +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.soloader.SoLoader + +import expo.modules.ApplicationLifecycleDispatcher +import expo.modules.ReactNativeHostWrapper + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( + this, + object : DefaultReactNativeHost(this) { + override fun getPackages(): List { + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(new MyReactNativePackage()); + return PackageList(this).packages + } + + override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + ) + + override val reactHost: ReactHost + get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + SoLoader.init(this, false) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + ApplicationLifecycleDispatcher.onApplicationCreate(this) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) + } +} diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_image.png b/android/app/src/main/res/drawable-hdpi/splashscreen_image.png index fe65e568..29815e62 100644 Binary files a/android/app/src/main/res/drawable-hdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-hdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_image.png b/android/app/src/main/res/drawable-mdpi/splashscreen_image.png index fe65e568..29815e62 100644 Binary files a/android/app/src/main/res/drawable-mdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-mdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-night-hdpi/splashscreen_image.png b/android/app/src/main/res/drawable-night-hdpi/splashscreen_image.png new file mode 100644 index 00000000..58aee83c Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/splashscreen_image.png b/android/app/src/main/res/drawable-night-mdpi/splashscreen_image.png new file mode 100644 index 00000000..58aee83c Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-night-xhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-night-xhdpi/splashscreen_image.png new file mode 100644 index 00000000..58aee83c Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-night-xxhdpi/splashscreen_image.png new file mode 100644 index 00000000..58aee83c Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-night-xxxhdpi/splashscreen_image.png new file mode 100644 index 00000000..58aee83c Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png index fe65e568..29815e62 100644 Binary files a/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png index fe65e568..29815e62 100644 Binary files a/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png index fe65e568..29815e62 100644 Binary files a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 036d09bc..f7258f46 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ - \ No newline at end of file + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 036d09bc..f7258f46 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ - \ No newline at end of file + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp index da4b4c1f..043bc8c5 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index 1236a66a..f7af698d 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 12fb0caa..a411475f 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp index b6881203..f98e57ee 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index c2860610..26ee4b6c 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 07ae99d4..3cdfdb14 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index e395b4f2..01d16ae6 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index c2be608b..41103c70 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 533bb62f..3883cc32 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index e6938322..75b16271 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index 6571abde..9a85f59d 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 13e9f3a8..f560947c 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 012fd69d..db6643ee 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index 7b74547e..d864add5 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 27c3d4f1..542ce168 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/resources.properties b/android/app/src/main/res/resources.properties new file mode 100644 index 00000000..d5a3ddc9 --- /dev/null +++ b/android/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US \ No newline at end of file diff --git a/android/app/src/release/java/app/neuland/ReactNativeFlipper.java b/android/app/src/release/java/app/neuland/ReactNativeFlipper.java deleted file mode 100644 index e2f98c1e..00000000 --- a/android/app/src/release/java/app/neuland/ReactNativeFlipper.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - *

This source code is licensed under the MIT license found in the LICENSE file in the root - * directory of this source tree. - */ -package app.neuland; - -import android.content.Context; -import com.facebook.react.ReactInstanceManager; - -/** - * Class responsible of loading Flipper inside your React Native application. This is the release - * flavor of it so it's empty as we don't want to load Flipper. - */ -public class ReactNativeFlipper { - public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { - // Do nothing as we don't want to initialize Flipper on Release. - } -} diff --git a/android/build.gradle b/android/build.gradle index bf861dbf..932bf7b3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,26 +2,27 @@ buildscript { ext { - buildToolsVersion = findProperty('android.buildToolsVersion') ?: '33.0.0' - minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21') - compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '33') - targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33') - kotlinVersion = findProperty('android.kotlinVersion') ?: '1.8.10' - frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0' + buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0' + minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23') + compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34') + targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') + kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23' - // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. - ndkVersion = "23.1.7779620" + ndkVersion = "26.1.10909125" } repositories { google() mavenCentral() } dependencies { - classpath('com.android.tools.build:gradle:7.4.2') + classpath('com.android.tools.build:gradle') classpath('com.facebook.react:react-native-gradle-plugin') + classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') } } +apply plugin: "com.facebook.react.rootproject" + allprojects { repositories { maven { @@ -30,7 +31,7 @@ allprojects { } maven { // Android JSC is installed from npm - url(new File(['node', '--print', "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), '../dist')) + url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist')) } google() diff --git a/android/gradle.properties b/android/gradle.properties index 8281ccbe..65ce3a08 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -25,9 +25,6 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true -# Version of flipper SDK to use with React Native -FLIPPER_VERSION=0.182.0 - # Use this property to specify which architecture you want to build. # You can also override it from the CLI using # ./gradlew -PreactNativeArchitectures=x86_64 @@ -55,5 +52,7 @@ expo.webp.animated=false # Enable network inspector EX_DEV_CLIENT_NETWORK_INSPECTOR=true -android.compileSdkVersion=34 -android.targetSdkVersion=34 \ No newline at end of file +# Use legacy packaging to compress native libraries in the resulting APK. +expo.useLegacyPackaging=false + +android.extraMavenRepos=[] \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar index 249e5832..d64cd491 100644 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 6ec1567a..2ea3535d 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew index a69d9cb6..1aa94a42 100755 --- a/android/gradlew +++ b/android/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/android/gradlew.bat b/android/gradlew.bat index f127cfd4..25da30db 100644 --- a/android/gradlew.bat +++ b/android/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -42,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/android/settings.gradle b/android/settings.gradle index 039c0355..4c85dc06 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,10 +1,18 @@ rootProject.name = 'Neuland Next' +dependencyResolutionManagement { + versionCatalogs { + reactAndroidLibs { + from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml"))) + } + } +} + apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); useExpoModules() -apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); +apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' -includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile()) +includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile()) diff --git a/app.config.ts b/app.config.ts index 5dd703cd..abbc164d 100644 --- a/app.config.ts +++ b/app.config.ts @@ -29,24 +29,27 @@ module.exports = { CFBundleDevelopmentRegion: 'en', }, splash: { - image: './src/assets/splash.png', + image: './src/assets/splash/splashLight.png', resizeMode: 'contain', backgroundColor: '#ffffff', dark: { + image: './src/assets/splash/splashDark.png', backgroundColor: '#000000', }, }, + icon: './src/assets/appIcons/default.png', }, android: { package: 'app.neuland', userInterfaceStyle: 'automatic', - versionCode: 78, + versionCode: 83, splash: { - image: './src/assets/splash.png', + image: './src/assets/splash/splashLight.png', resizeMode: 'contain', backgroundColor: '#ffffff', dark: { backgroundColor: '#000000', + image: './src/assets/splash/splashDark.png', }, }, }, @@ -84,6 +87,7 @@ module.exports = { }, ], ['expo-build-properties'], + ['@maplibre/maplibre-react-native'], ], extra: { eas: { diff --git a/bun.lockb b/bun.lockb index 05bc10bc..54544f20 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 0bc549be..6053cecd 100644 --- a/ios/NeulandNext.xcodeproj/project.pbxproj +++ b/ios/NeulandNext.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 48253CD9557284557A309B5B /* libPods-NeulandNext.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BE89E54F9B2F4C95465DBC97 /* libPods-NeulandNext.a */; }; 4DBFAAE1E5644C68B67780C7 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25124D82DC7147C1A383882B /* noop-file.swift */; }; B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; + B77BAE7A2C2AD20500BF7D99 /* MapLibre in Frameworks */ = {isa = PBXBuildFile; productRef = 593828D4C3990B605265629B /* MapLibre */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; /* End PBXBuildFile section */ @@ -42,6 +43,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B77BAE7A2C2AD20500BF7D99 /* MapLibre in Frameworks */, 48253CD9557284557A309B5B /* libPods-NeulandNext.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -58,8 +60,8 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB71A68108700A75B9A /* main.m */, - AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, 25124D82DC7147C1A383882B /* noop-file.swift */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, 4CF7375F2B7F45769EF88A59 /* NeulandNext-Bridging-Header.h */, 6D9C36C5590640DA61E545A1 /* PrivacyInfo.xcprivacy */, ); @@ -155,12 +157,20 @@ 7DD2A0B2CBB9482199BAE73B /* Upload Debug Symbols to Sentry */, 4DEE41591059C6E17AF12B0C /* [CP] Embed Pods Frameworks */, FC6914C1B30E9D871353D326 /* [CP] Copy Pods Resources */, + 5521C5867CF044158CA46F7B /* Remove signature files (Xcode 15 workaround) */, + DE94DDD297BF42F2B5882A0E /* Remove signature files (Xcode 15 workaround) */, + 416B75F9501048ACA40DB650 /* Remove signature files (Xcode 15 workaround) */, + EF820F10D3E648A28C78DECD /* Remove signature files (Xcode 15 workaround) */, + 1515044FA4184334AD7857BB /* Remove signature files (Xcode 15 workaround) */, ); buildRules = ( ); dependencies = ( ); name = NeulandNext; + packageProductDependencies = ( + 593828D4C3990B605265629B /* MapLibre */, + ); productName = NeulandNext; productReference = 13B07F961A680F5B00A75B9A /* NeulandNext.app */; productType = "com.apple.product-type.application"; @@ -189,6 +199,9 @@ Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; + packageReferences = ( + E90CD075DD22378B5CD3BEC5 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */, + ); productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -228,6 +241,20 @@ shellPath = /bin/sh; shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; }; + 416B75F9501048ACA40DB650 /* 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"; + }; 4770EA3B2F9A4E615EFE9B20 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -265,6 +292,20 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeulandNext/Pods-NeulandNext-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 5521C5867CF044158CA46F7B /* 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"; + }; 7DD2A0B2CBB9482199BAE73B /* Upload Debug Symbols to Sentry */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -301,6 +342,34 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + DE94DDD297BF42F2B5882A0E /* 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"; + }; + EF820F10D3E648A28C78DECD /* 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"; + }; FC6914C1B30E9D871353D326 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -383,6 +452,23 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeulandNext/Pods-NeulandNext-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 1515044FA4184334AD7857BB /* 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 */ @@ -424,7 +510,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.7.2; + MARKETING_VERSION = 0.8.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -432,7 +518,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; @@ -453,6 +539,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = FSXB76X6V2; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64"; INFOPLIST_FILE = NeulandNext/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; IPHONEOS_DEPLOYMENT_TARGET = 13.4; @@ -460,7 +547,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.7.2; + MARKETING_VERSION = 0.8.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -468,7 +555,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"; @@ -535,10 +622,7 @@ 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; @@ -596,10 +680,7 @@ ); 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; @@ -629,6 +710,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + E90CD075DD22378B5CD3BEC5 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/maplibre/maplibre-gl-native-distribution"; + requirement = { + kind = exactVersion; + version = 6.4.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 593828D4C3990B605265629B /* MapLibre */ = { + isa = XCSwiftPackageProductDependency; + package = E90CD075DD22378B5CD3BEC5 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */; + productName = MapLibre; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } diff --git a/ios/NeulandNext.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/NeulandNext.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..d223fad1 --- /dev/null +++ b/ios/NeulandNext.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "b440cbd994821d1c59ef3fae39cdd359458004248a336c94f8a8b08f5fed3877", + "pins" : [ + { + "identity" : "maplibre-gl-native-distribution", + "kind" : "remoteSourceControl", + "location" : "https://github.com/maplibre/maplibre-gl-native-distribution", + "state" : { + "revision" : "6d0071977ed1f2380c739715f82ac650f99b0824", + "version" : "6.4.0" + } + } + ], + "version" : 3 +} diff --git a/ios/NeulandNext/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/NeulandNext/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png index 7a74ea3c..fb4ea755 100644 Binary files a/ios/NeulandNext/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png and b/ios/NeulandNext/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/ios/NeulandNext/Images.xcassets/Default.appiconset/default.png b/ios/NeulandNext/Images.xcassets/Default.appiconset/default.png index 139f0215..fb4ea755 100644 Binary files a/ios/NeulandNext/Images.xcassets/Default.appiconset/default.png and b/ios/NeulandNext/Images.xcassets/Default.appiconset/default.png differ diff --git a/ios/NeulandNext/Images.xcassets/ModernLight.appiconset/modernLight.png b/ios/NeulandNext/Images.xcassets/ModernLight.appiconset/modernLight.png deleted file mode 100644 index 9faba57e..00000000 Binary files a/ios/NeulandNext/Images.xcassets/ModernLight.appiconset/modernLight.png and /dev/null differ diff --git a/ios/NeulandNext/Images.xcassets/ModernPink.appiconset/Contents.json b/ios/NeulandNext/Images.xcassets/ModernPink.appiconset/Contents.json deleted file mode 100644 index 05817e34..00000000 --- a/ios/NeulandNext/Images.xcassets/ModernPink.appiconset/Contents.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "images" : [ - { - "filename" : "pink_dark.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/NeulandNext/Images.xcassets/ModernPink.appiconset/pink_dark.png b/ios/NeulandNext/Images.xcassets/ModernPink.appiconset/pink_dark.png deleted file mode 100644 index e6f6f7f8..00000000 Binary files a/ios/NeulandNext/Images.xcassets/ModernPink.appiconset/pink_dark.png and /dev/null differ diff --git a/ios/NeulandNext/Images.xcassets/ModernLight.appiconset/Contents.json b/ios/NeulandNext/Images.xcassets/Retro.appiconset/Contents.json similarity index 83% rename from ios/NeulandNext/Images.xcassets/ModernLight.appiconset/Contents.json rename to ios/NeulandNext/Images.xcassets/Retro.appiconset/Contents.json index bd2994df..5aa2698f 100644 --- a/ios/NeulandNext/Images.xcassets/ModernLight.appiconset/Contents.json +++ b/ios/NeulandNext/Images.xcassets/Retro.appiconset/Contents.json @@ -1,7 +1,7 @@ { "images": [ { - "filename": "modernLight.png", + "filename": "retro.png", "idiom": "universal", "platform": "ios", "size": "1024x1024" diff --git a/ios/NeulandNext/Images.xcassets/Retro.appiconset/retro.png b/ios/NeulandNext/Images.xcassets/Retro.appiconset/retro.png new file mode 100644 index 00000000..7a74ea3c Binary files /dev/null and b/ios/NeulandNext/Images.xcassets/Retro.appiconset/retro.png differ diff --git a/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/Contents.json b/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/Contents.json index 3cf84897..7cbe3f08 100644 --- a/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/Contents.json +++ b/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/Contents.json @@ -12,6 +12,37 @@ { "idiom": "universal", "scale": "3x" + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "filename": "dark_image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "scale": "2x" + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "scale": "3x" } ], "info": { diff --git a/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/dark_image.png b/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/dark_image.png new file mode 100644 index 00000000..58aee83c Binary files /dev/null and b/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/dark_image.png differ diff --git a/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/image.png b/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/image.png index fe65e568..29815e62 100644 Binary files a/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/image.png and b/ios/NeulandNext/Images.xcassets/SplashScreen.imageset/image.png differ diff --git a/ios/NeulandNext/Info.plist b/ios/NeulandNext/Info.plist index 33b79ec0..7f2febdb 100644 --- a/ios/NeulandNext/Info.plist +++ b/ios/NeulandNext/Info.plist @@ -43,20 +43,11 @@ UIPrerenderedIcon - ModernLight + Retro CFBundleIconFiles - ModernLight - - UIPrerenderedIcon - - - ModernPink - - CFBundleIconFiles - - ModernPink + Retro UIPrerenderedIcon @@ -95,7 +86,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 0.7.2 + 0.8.1 CFBundleSignature ???? CFBundleURLTypes @@ -139,6 +130,11 @@ $(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 + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route RCTAsyncStorageExcludeFromBackup diff --git a/ios/Podfile b/ios/Podfile index bc000b39..28239583 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -29,6 +29,9 @@ target 'NeulandNext' do ) post_install do |installer| +# @generated begin @maplibre/maplibre-react-native-post_installer - expo prebuild (DO NOT MODIFY) sync-72b8976cc2231a4441a5b54389fb6e10bd42a1be + $RCTMLN.post_install(installer) +# @generated end @maplibre/maplibre-react-native-post_installer react_native_post_install( installer, config[:reactNativePath], diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a9d0887b..4cafc183 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -313,6 +313,13 @@ PODS: - hermes-engine (0.74.2): - hermes-engine/Pre-built (= 0.74.2) - hermes-engine/Pre-built (0.74.2) + - maplibre-react-native (10.0.0-alpha.5): + - maplibre-react-native/DynamicLibrary (= 10.0.0-alpha.5) + - React + - React-Core + - maplibre-react-native/DynamicLibrary (10.0.0-alpha.5): + - React + - React-Core - RCT-Folly (2024.01.01.00): - boost - DoubleConversion @@ -1245,8 +1252,6 @@ PODS: - React - react-native-drag-drop-ios (0.1.1): - React-Core - - react-native-menu (0.9.1): - - React - react-native-netinfo (11.3.1): - React-Core - react-native-pager-view (6.3.0): @@ -1673,6 +1678,7 @@ DEPENDENCIES: - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - "maplibre-react-native (from `../node_modules/@maplibre/maplibre-react-native`)" - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) @@ -1701,7 +1707,6 @@ DEPENDENCIES: - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - react-native-context-menu-view (from `../node_modules/react-native-context-menu-view`) - react-native-drag-drop-ios (from `../node_modules/react-native-drag-drop-ios`) - - "react-native-menu (from `../node_modules/@react-native-menu/menu`)" - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -1830,6 +1835,8 @@ EXTERNAL SOURCES: hermes-engine: :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-2024-06-03-RNv0.74.2-bb1e74fe1e95c2b5a2f4f9311152da052badc2bc + maplibre-react-native: + :path: "../node_modules/@maplibre/maplibre-react-native" RCT-Folly: :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: @@ -1882,8 +1889,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-context-menu-view" react-native-drag-drop-ios: :path: "../node_modules/react-native-drag-drop-ios" - react-native-menu: - :path: "../node_modules/@react-native-menu/menu" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-pager-view: @@ -2009,6 +2014,7 @@ SPEC CHECKSUMS: fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f hermes-engine: 01d3e052018c2a13937aca1860fbedbccd4a41b7 + maplibre-react-native: bcb8d0ed567f4a67f651e2d1a24521ef273c23de RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47 RCTDeprecation: b03c35057846b685b3ccadc9bfe43e349989cdb2 RCTRequired: 194626909cfa8d39ca6663138c417bc6c431648c @@ -2035,7 +2041,6 @@ SPEC CHECKSUMS: React-Mapbuffer: bf56147c9775491e53122a94c423ac201417e326 react-native-context-menu-view: 30915369a9b5887904c571b616653acf3f1c8edb react-native-drag-drop-ios: 099293de149185e9116a057a8b6b98f95ce9b56e - react-native-menu: 9728f90160c36b9a75481fc76e05354b99d54c59 react-native-netinfo: bdb108d340cdb41875c9ced535977cac6d2ff321 react-native-pager-view: c1e29e1a6105a02807392ba822ad322447a72f55 react-native-safe-area-context: dcab599c527c2d7de2d76507a523d20a0b83823d @@ -2081,6 +2086,6 @@ SPEC CHECKSUMS: UMAppLoader: f17a5ee8e85b536ace0fc254b447a37ed198d57e Yoga: ae3c32c514802d30f687a04a6a35b348506d411f -PODFILE CHECKSUM: 3a09a7a2ba19331e6aa5922ccde95dc10663a4a3 +PODFILE CHECKSUM: a2c2df1b6d6fb8a577a08d33880679b64c476742 COCOAPODS: 1.14.3 diff --git a/ios/ci_scripts/ci_post_clone.sh b/ios/ci_scripts/ci_post_clone.sh new file mode 100755 index 00000000..78355c98 --- /dev/null +++ b/ios/ci_scripts/ci_post_clone.sh @@ -0,0 +1,15 @@ +#!/bin/zsh + +echo "===== Installling CocoaPods =====" +export HOMEBREW_NO_INSTALL_CLEANUP=TRUE +brew install cocoapods +echo "===== Installing Node.js =====" +brew install node@22 + +# Install dependencies +echo "===== Running bun install =====" +cd .. +npm install +echo "===== Running pod install =====" +cd ios +pod install \ No newline at end of file diff --git a/package.json b/package.json index b53c7acc..8e3c4e61 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "version": "0.7.2", + "version": "0.8.1", "name": "neuland", "main": "expo-router/entry", "homepage": "https://github.com/neuland-ingolstadt/neuland.app-native", "private": true, "scripts": { - "start": "expo start -c", + "start": "EXPO_USE_FAST_RESOLVER=1 expo", "tunnel": "expo start -c --tunnel", "android": "expo run:android", "ios": "expo run:ios", @@ -27,17 +27,17 @@ "@expo/vector-icons": "^14.0.0", "@gorhom/bottom-sheet": "^4", "@kichiyaki/react-native-barcode-generator": "^0.6.7", + "@maplibre/maplibre-react-native": "^10.0.0-alpha.5", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/datetimepicker": "8.0.1", "@react-native-community/netinfo": "11.3.1", - "@react-native-menu/menu": "^0.9.1", "@sentry/react-native": "~5.22.0", "@shopify/flash-list": "1.6.4", "@tanstack/query-async-storage-persister": "^5.17.19", "@tanstack/react-query": "^5.17.19", "@tanstack/react-query-persist-client": "^5.17.19", "color": "^4.2.3", - "expo": "^51.0.11", + "expo": "^51.0.14", "expo-blur": "~13.0.2", "expo-brightness": "~12.0.1", "expo-build-properties": "~0.12.3", @@ -53,7 +53,7 @@ "expo-location": "~17.0.1", "expo-navigation-bar": "~3.0.6", "expo-notifications": "~0.28.7", - "expo-router": "~3.5.15", + "expo-router": "~3.5.16", "expo-secure-store": "~13.0.1", "expo-sharing": "~12.0.1", "expo-splash-screen": "~0.27.5", @@ -105,6 +105,7 @@ "@expo/ngrok": "^4.1.0", "@trivago/prettier-plugin-sort-imports": "^4.2.1", "@types/color": "^3.0.5", + "@types/geojson": "^7946.0.14", "@types/prop-types": "^15", "@types/react": "~18.2.45", "@types/react-native-actionsheet": "^2", @@ -121,6 +122,7 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "latest", "eslint-plugin-react-native": "^4.1.0", + "expo-atlas": "^0.3.0", "husky": "^9.0.6", "lint-staged": ">=15", "prettier": "3.2.5", diff --git a/src/api/neuland-api.ts b/src/api/neuland-api.ts index 16dab59d..e9fe0dde 100644 --- a/src/api/neuland-api.ts +++ b/src/api/neuland-api.ts @@ -3,7 +3,7 @@ import { gql, request } from 'graphql-request' import packageInfo from '../../package.json' -const GRAPHQL_ENDPOINT: string = 'https://api.neuland.app/graphql' +const GRAPHQL_ENDPOINT: string = 'https://api.dev.neuland.app/graphql' const ASSET_ENDPOINT: string = 'https://assets.neuland.app' const USER_AGENT = `neuland.app-native/${packageInfo.version} (+${packageInfo.homepage})` diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index adf18aa9..19cfd870 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -23,6 +23,7 @@ import { Platform, StyleSheet } from 'react-native' import Shortcuts, { type ShortcutItem } from 'rn-quick-actions' import { humanLocations } from '../(food)/meal' +import { appIcons } from '../(user)/appicon' import packageInfo from '../../../package.json' export default function HomeLayout(): JSX.Element { @@ -32,7 +33,7 @@ export default function HomeLayout(): JSX.Element { const flow = React.useContext(FlowContext) const { t } = useTranslation('navigation') const { selectedRestaurants } = useContext(FoodFilterContext) - const { appIcon } = useContext(AppIconContext) + const { appIcon, toggleAppIcon } = useContext(AppIconContext) const aptabaseKey = process.env.EXPO_PUBLIC_APTABASE_KEY const { analyticsAllowed, initializeAnalytics } = React.useContext(FlowContext) @@ -185,6 +186,14 @@ export default function HomeLayout(): JSX.Element { } }, [analyticsAllowed]) + useEffect(() => { + if (Platform.OS !== 'ios') return + + if (!appIcons.includes(appIcon)) { + toggleAppIcon('default') + } + }, [appIcon]) + return ( <> (null) - const [currentFloor, setCurrentFloor] = useState('EG') + const [currentFloor, setCurrentFloor] = useState({ + floor: 'EG', + manual: false, + }) const [nextLecture, setNextLecture] = useState< FriendlyTimetableEntry[] | null >(null) diff --git a/src/app/(user)/appicon.tsx b/src/app/(user)/appicon.tsx index 07fcb3cc..d7dfe6c2 100644 --- a/src/app/(user)/appicon.tsx +++ b/src/app/(user)/appicon.tsx @@ -27,15 +27,16 @@ let iconImages: Record = {} iconImages = { default: require('@/assets/appIcons/default.png'), modernDark: require('@/assets/appIcons/modernDark.png'), - modernLight: require('@/assets/appIcons/modernLight.png'), + retro: require('@/assets/appIcons/retro.png'), modernGreen: require('@/assets/appIcons/modernGreen.png'), - modernPink: require('@/assets/appIcons/modernPink.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 const appIcons = Object.keys(iconImages) + export default function AppIconPicker(): JSX.Element { const colors = useTheme().colors as Colors const { appIcon, toggleAppIcon, unlockedAppIcons } = @@ -43,8 +44,8 @@ export default function AppIconPicker(): JSX.Element { const { t } = useTranslation(['settings']) const isModal = useLocalSearchParams().fromAppShortcut === 'true' const categories: Record = { - exclusive: ['cat', 'modernPink'], - default: ['default', 'modernLight', 'modernDark', 'modernGreen'], + exclusive: ['cat', 'retro'], + default: ['default', 'modernDark', 'modernGreen'], rainbow: ['rainbowNeon', 'rainbowDark', 'rainbowMoonLight'], } diff --git a/src/app/(user)/theme.tsx b/src/app/(user)/theme.tsx index 14561a04..d5526feb 100644 --- a/src/app/(user)/theme.tsx +++ b/src/app/(user)/theme.tsx @@ -25,8 +25,7 @@ if (Platform.OS === 'ios') { iconImages = { default: require('@/assets/appIcons/default.png'), modernDark: require('@/assets/appIcons/modernDark.png'), - modernLight: require('@/assets/appIcons/modernLight.png'), - modernPink: require('@/assets/appIcons/modernPink.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'), diff --git a/src/assets/appIcons/default.png b/src/assets/appIcons/default.png index 139f0215..9faba57e 100644 Binary files a/src/assets/appIcons/default.png and b/src/assets/appIcons/default.png differ diff --git a/src/assets/appIcons/modernLight.png b/src/assets/appIcons/modernLight.png deleted file mode 100644 index 9faba57e..00000000 Binary files a/src/assets/appIcons/modernLight.png and /dev/null differ diff --git a/src/assets/appIcons/modernPink.png b/src/assets/appIcons/modernPink.png deleted file mode 100644 index e6f6f7f8..00000000 Binary files a/src/assets/appIcons/modernPink.png and /dev/null differ diff --git a/src/assets/appIcons/retro.png b/src/assets/appIcons/retro.png new file mode 100644 index 00000000..139f0215 Binary files /dev/null and b/src/assets/appIcons/retro.png differ diff --git a/src/assets/splash/splashDark.png b/src/assets/splash/splashDark.png new file mode 100644 index 00000000..069f3ddc Binary files /dev/null and b/src/assets/splash/splashDark.png differ diff --git a/src/assets/splash/splashLight.png b/src/assets/splash/splashLight.png new file mode 100644 index 00000000..41251d31 Binary files /dev/null and b/src/assets/splash/splashLight.png differ diff --git a/src/components/Elements/Food/MealEntry.tsx b/src/components/Elements/Food/MealEntry.tsx index 398e22c8..b0418e53 100644 --- a/src/components/Elements/Food/MealEntry.tsx +++ b/src/components/Elements/Food/MealEntry.tsx @@ -81,7 +81,7 @@ export const MealEntry = ({ const iconName = hasUserAllergens ? 'exclamationmark.triangle' : 'info.circle' - const androidName = hasUserAllergens ? 'warning' : 'check' + const androidName = hasUserAllergens ? 'warning' : 'info' const textContent = hasUserAllergens ? userAllergens : t('empty.noAllergens') @@ -275,6 +275,7 @@ export const MealEntry = ({ android={{ name: androidName, size: 16, + variant: 'outlined', }} style={styles.icon} color={colors.notification} diff --git a/src/components/Elements/Map/BottomSheetBackground.tsx b/src/components/Elements/Map/BottomSheetBackground.tsx index bb96d7cc..4d3f8d86 100644 --- a/src/components/Elements/Map/BottomSheetBackground.tsx +++ b/src/components/Elements/Map/BottomSheetBackground.tsx @@ -6,7 +6,7 @@ import { Platform, StyleSheet, View } from 'react-native' const BottomSheetBackground = (): JSX.Element => { const { colors, dark } = useTheme() const darkIos = 'rgba(0, 0, 0, 0.55)' - const lightIos = 'rgba(255, 255, 255, 0.2)' + const lightIos = 'rgba(200, 200, 200, 0.2)' return Platform.OS === 'ios' ? ( - handleSheetChangesModal: (index: number) => void + handleSheetChangesModal: () => void currentPositionModal: SharedValue roomData: RoomData modalSection: FormListSections[] @@ -47,7 +47,6 @@ const handleShareModal = (room: string): void => { * BottomSheetDetailModal component for displaying room details * @param {React.RefObject} bottomSheetModalRef - Reference to the bottom sheet modal * @param {Function} handleSheetChangesModal - Function to handle changes in the bottom sheet modal - * @param {SharedValue} currentPositionModal - Current position of the bottom sheet modal * @param {any} roomData - Data for the room * @param {FormListSections[]} modalSection - Sections for the room * @returns {JSX.Element} @@ -66,7 +65,7 @@ export const BottomSheetDetailModal = ({ index={1} ref={bottomSheetModalRef} snapPoints={['15%', '30%', '45%', '70%']} - onChange={handleSheetChangesModal} + onDismiss={handleSheetChangesModal} backgroundComponent={BottomSheetBackground} animatedPosition={currentPositionModal} > diff --git a/src/components/Elements/Map/BottomSheetMap.tsx b/src/components/Elements/Map/BottomSheetMap.tsx index 7f45090f..ee7c7aa8 100644 --- a/src/components/Elements/Map/BottomSheetMap.tsx +++ b/src/components/Elements/Map/BottomSheetMap.tsx @@ -1,22 +1,23 @@ /* eslint-disable react-native/no-color-literals */ import { type Colors } from '@/components/colors' -import { UserKindContext } from '@/components/contexts' +import { AppIconContext, UserKindContext } from '@/components/contexts' import { MapContext } from '@/hooks/contexts/map' import { USER_GUEST } from '@/hooks/contexts/userKind' import { SEARCH_TYPES } from '@/types/map' -import { type RoomEntry } from '@/types/utils' import { formatFriendlyDate, formatFriendlyTime } from '@/utils/date-utils' -import { getCenterSingle } from '@/utils/map-utils' import { PAGE_BOTTOM_SAFE_AREA, PAGE_PADDING } from '@/utils/style-utils' import { getContrastColor } from '@/utils/ui-utils' import BottomSheet, { BottomSheetTextInput } from '@gorhom/bottom-sheet' import { useTheme } from '@react-navigation/native' import { useRouter } from 'expo-router' import Fuse from 'fuse.js' -import React, { useContext, useMemo } from 'react' +import { type FeatureCollection } from 'geojson' +import React, { useContext, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { ActivityIndicator, + Alert, + Linking, Platform, Pressable, SectionList, @@ -26,20 +27,17 @@ import { View, } from 'react-native' import { type SharedValue } from 'react-native-reanimated' -import type WebView from 'react-native-webview' import Divider from '../Universal/Divider' import PlatformIcon from '../Universal/Icon' import BottomSheetBackground from './BottomSheetBackground' import ResultRow from './SearchResultRow' -import { _injectMarker, _setView } from './leaflet' interface MapBottomSheetProps { bottomSheetRef: React.RefObject currentPosition: SharedValue handlePresentModalPress: () => void - allRooms: RoomEntry[] - mapRef: React.RefObject + allRooms: FeatureCollection } const MapBottomSheet: React.FC = ({ @@ -47,7 +45,6 @@ const MapBottomSheet: React.FC = ({ currentPosition, handlePresentModalPress, allRooms, - mapRef, }) => { const router = useRouter() const colors = useTheme().colors as Colors @@ -62,7 +59,9 @@ const MapBottomSheet: React.FC = ({ setCurrentFloor, } = useContext(MapContext) - const fuse = new Fuse(allRooms, { + const { unlockedAppIcons, addUnlockedAppIcon } = useContext(AppIconContext) + + const fuse = new Fuse(allRooms.features, { keys: [ 'properties.Raum', i18n.language === 'de' @@ -77,10 +76,12 @@ const MapBottomSheet: React.FC = ({ const [searchResultsExact, searchResultsFuzzy] = useMemo(() => { const results = fuse.search(localSearch.trim().toUpperCase()) const roomResults = results.map((result) => ({ - title: result.item.properties.Raum, - subtitle: result.item.properties.Funktion_en, - isExactMatch: result.item.properties.Raum.toUpperCase().includes( - localSearch.toUpperCase() + title: result.item.properties?.Raum, + subtitle: result.item.properties?.Funktion_en, + isExactMatch: Boolean( + result.item.properties?.Raum.toUpperCase().includes( + localSearch.toUpperCase() + ) ), item: result.item, })) @@ -93,6 +94,31 @@ const MapBottomSheet: React.FC = ({ return [exactMatches, fuzzyMatches] }, [localSearch, allRooms]) + useEffect(() => { + if ( + localSearch.toLocaleLowerCase() === 'neuland' && + Platform.OS === 'ios' + ) { + if (unlockedAppIcons.includes('retro')) { + return + } + Alert.alert( + t('pages.map.easterEgg.title'), + + t('pages.map.easterEgg.message'), + [ + { + text: t('pages.map.easterEgg.confirm'), + style: 'cancel', + }, + ], + { cancelable: false } + ) + + addUnlockedAppIcon('retro') + } + }, [localSearch]) + return ( = ({ animatedPosition={currentPosition} keyboardBehavior="extend" > - - {Platform.OS === 'ios' ? ( - { - setLocalSearch(text) - }} - onFocus={() => { - bottomSheetRef.current?.snapToIndex(2) - }} - onEndEditing={() => { - bottomSheetRef.current?.collapse() - }} - /> - ) : ( - { - setLocalSearch(text) - }} - onFocus={() => { - bottomSheetRef.current?.snapToIndex(2) - }} - onEndEditing={() => { - bottomSheetRef.current?.collapse() - }} - /> - )} - {localSearch !== '' ? ( - searchResultsExact.length > 0 || - searchResultsFuzzy.length > 0 ? ( - + + {Platform.OS === 'ios' ? ( + { + setLocalSearch(text) + }} + onFocus={() => { + bottomSheetRef.current?.snapToIndex(2) + }} + onEndEditing={() => { + bottomSheetRef.current?.collapse() }} - keyboardShouldPersistTaps="always" - sections={[ - ...(searchResultsExact.length > 0 - ? [ - { - title: t( - 'pages.map.search.results' - ), - data: searchResultsExact, - }, - ] - : []), - ...(searchResultsFuzzy.length > 0 - ? [ - { - title: t( - 'pages.map.search.fuzzy' - ), - data: searchResultsFuzzy, - }, - ] - : []), - ]} - keyExtractor={(item, index) => item.title + index} - renderItem={({ item, index }) => ( - - )} - stickySectionHeadersEnabled={false} - renderSectionHeader={({ section: { title } }) => ( - - {title} - - )} /> ) : ( - - {t('pages.map.search.noResults')} - - ) - ) : userKind === USER_GUEST ? ( - - {t('pages.map.details.room.signIn')} - - ) : ( - <> - {nextLecture !== null && nextLecture.length > 0 && ( - - + placeholder={t('pages.map.search.placeholder')} + placeholderTextColor={colors.labelColor} + value={localSearch} + enablesReturnKeyAutomatically + clearButtonMode="always" + onChangeText={(text) => { + setLocalSearch(text) + }} + onFocus={() => { + bottomSheetRef.current?.snapToIndex(2) + }} + onEndEditing={() => { + bottomSheetRef.current?.collapse() + }} + /> + )} + {localSearch !== '' ? ( + searchResultsExact.length > 0 || + searchResultsFuzzy.length > 0 ? ( + 0 + ? [ + { + title: t( + 'pages.map.search.results' + ), + data: searchResultsExact, + }, + ] + : []), + ...(searchResultsFuzzy.length > 0 + ? [ + { + title: t( + 'pages.map.search.fuzzy' + ), + data: searchResultsFuzzy, + }, + ] + : []), + ]} + keyExtractor={(item, index) => + item.title + index + } + renderItem={({ item, index }) => ( + + )} + stickySectionHeadersEnabled={false} + renderSectionHeader={({ + section: { title }, + }) => ( - {t( - 'pages.map.details.room.nextLecture' - )} - - - {formatFriendlyDate( - nextLecture[0].date - )} - - - - {nextLecture.map((lecture, key) => ( - <> - { - const details = - allRooms.find( - (x) => - x.properties - .Raum === - lecture.rooms[0] - ) - - if ( - details?.coordinates !== - undefined - ) { - const center = - getCenterSingle( - details?.coordinates - ) - _setView(center, mapRef) - _injectMarker( - mapRef, - center, - colors - ) - } - - const etage = - details?.properties - .Ebene - - setCurrentFloor( - etage ?? 'EG' - ) - setClickedElement({ - data: lecture.rooms[0], - type: SEARCH_TYPES.ROOM, - }) - - handlePresentModalPress() - bottomSheetRef.current?.close() - }} - > - - - - - - - - {lecture.name} - - - {lecture.rooms.join( - ', ' - )} - - - - - - {formatFriendlyTime( - lecture.startDate - )} - - - {formatFriendlyTime( - lecture.endDate - )} - - - - {key !== nextLecture.length - 1 && ( - - )} - - ))} - - - )} - - - - - {t('pages.map.details.room.availableRooms')} - - { - router.push('(map)/advanced') - }} - hitSlop={{ - top: 10, - right: 10, - bottom: 10, - left: 10, - }} - > - - {t('misc.more')} + {title} - - - + ) : ( + - {availableRooms === null ? ( - - ) : availableRooms.length === 0 ? ( - + ) + ) : userKind === USER_GUEST ? ( + + {t('pages.map.details.room.signIn')} + + ) : ( + <> + {nextLecture !== null && nextLecture.length > 0 && ( + + + + {t( + 'pages.map.details.room.nextLecture' + )} + + + {formatFriendlyDate( + nextLecture[0].date + )} + + + - {t('pages.map.noAvailableRooms')} - - ) : ( - availableRooms - .slice(0, 3) - .map((room, key) => ( - <> + {nextLecture.map((lecture, key) => ( + { const details = - allRooms.find( + allRooms.features.find( (x) => x.properties - .Raum === - room.room + ?.Raum === + lecture + .rooms[0] ) - if ( - details?.coordinates !== - undefined - ) { - const center = - getCenterSingle( - details?.coordinates - ) - _setView( - center, - mapRef - ) - _injectMarker( - mapRef, - center, - colors - ) - } - const etage = details?.properties - .Ebene - - setCurrentFloor( - etage ?? 'EG' - ) + ?.Ebene + bottomSheetRef.current?.close() + setCurrentFloor({ + floor: + (etage as string) ?? + 'EG', + manual: false, + }) setClickedElement({ - data: room.room, + data: lecture + .rooms[0], type: SEARCH_TYPES.ROOM, + center: details + ?.properties + ?.center, + manual: false, }) - handlePresentModalPress() - bottomSheetRef.current?.close() }} > = ({ colors.primary )} ios={{ - name: 'studentdesk', + name: 'clock.fill', size: 18, }} android={{ @@ -531,14 +356,21 @@ const MapBottomSheet: React.FC = ({ /> - + - {room.room} + {lecture.name} = ({ ...styles.suggestionSubtitle, }} > - {room.type} ( - {room.capacity}{' '} - seats) + {lecture.rooms.join( + ', ' + )} @@ -566,7 +398,7 @@ const MapBottomSheet: React.FC = ({ }} > {formatFriendlyTime( - room.from + lecture.startDate )} = ({ }} > {formatFriendlyTime( - room.until + lecture.endDate )} - {key !== 2 && } - - )) - )} + {key !== + nextLecture.length - 1 && ( + + )} + + ))} + + + )} + + + + + {t( + 'pages.map.details.room.availableRooms' + )} + + { + router.push('(map)/advanced') + }} + hitSlop={{ + top: 10, + right: 10, + bottom: 10, + left: 10, + }} + > + + {t('misc.more')} + + + + + {availableRooms === null ? ( + + ) : availableRooms.length === 0 ? ( + + {t('pages.map.noAvailableRooms')} + + ) : ( + availableRooms + .slice(0, 3) + .map((room, key) => ( + + { + const details = + allRooms.features.find( + (x) => + x + .properties + ?.Raum === + room.room + ) + + const etage = + details + ?.properties + ?.Ebene + + setCurrentFloor({ + floor: + (etage as string) ?? + 'EG', + manual: false, + }) + setClickedElement({ + data: room.room, + type: SEARCH_TYPES.ROOM, + center: + details + ?.properties + ?.center ?? + undefined, + manual: false, + }) + + handlePresentModalPress() + bottomSheetRef.current?.close() + }} + > + + + + + + + + {room.room} + + + {room.type}{' '} + ( + { + room.capacity + }{' '} + seats) + + + + + + {formatFriendlyTime( + room.from + )} + + + {formatFriendlyTime( + room.until + )} + + + + {key !== 2 && } + + )) + )} + - - - )} + + )} + + + { + void Linking.openURL( + 'https://www.openstreetmap.org/copyright' + ) + }} + style={styles.attributionLink} + > + + {t('pages.map.details.osm')} + + + + ) @@ -605,10 +670,14 @@ const styles = StyleSheet.create({ suggestionSectionHeader: { fontWeight: '600', fontSize: 20, - marginTop: 6, + paddingTop: 8, marginBottom: 2, textAlign: 'left', }, + suggestionContent: { + flex: 1, + paddingRight: 14, + }, suggestionSectionHeaderContainer: { flexDirection: 'row', justifyContent: 'space-between', @@ -635,13 +704,15 @@ const styles = StyleSheet.create({ fontSize: 17, }, suggestionRow: { - padding: 10, + paddingVertical: 10, + paddingHorizontal: 12, flexDirection: 'row', - justifyContent: 'space-between', }, suggestionInnerRow: { flexDirection: 'row', alignItems: 'center', + justifyContent: 'space-between', + flex: 1, }, suggestionIconContainer: { marginRight: 14, @@ -655,7 +726,6 @@ const styles = StyleSheet.create({ fontWeight: '600', fontSize: 16, marginBottom: 1, - maxWidth: '90%', }, suggestionSubtitle: { fontWeight: '400', @@ -682,4 +752,14 @@ const styles = StyleSheet.create({ loadingMargin: { marginVertical: 30, }, + attributionContainer: { paddingVertical: 40 }, + attributionLink: { + flexDirection: 'row', + gap: 4, + alignItems: 'center', + }, + attributionText: { + fontSize: 15, + paddingStart: 12, + }, }) diff --git a/src/components/Elements/Map/FloorPicker.tsx b/src/components/Elements/Map/FloorPicker.tsx index 6a19e7d9..e891675d 100644 --- a/src/components/Elements/Map/FloorPicker.tsx +++ b/src/components/Elements/Map/FloorPicker.tsx @@ -2,86 +2,29 @@ import { type Colors } from '@/components/colors' import { MapContext } from '@/hooks/contexts/map' import { useTheme } from '@react-navigation/native' import * as Haptics from 'expo-haptics' -import { getCurrentPositionAsync } from 'expo-location' -import * as Location from 'expo-location' import React, { useContext } from 'react' -import { useTranslation } from 'react-i18next' -import { - Alert, - Linking, - Platform, - Pressable, - StyleSheet, - Text, - View, -} from 'react-native' -import { type WebView } from 'react-native-webview' +import { Platform, Pressable, StyleSheet, Text, View } from 'react-native' import PlatformIcon from '../Universal/Icon' -import { _injectCurrentLocation, _setView } from './leaflet' interface FloorPickerProps { floors: string[] showAllFloors: boolean toggleShowAllFloors: () => void - mapRef: React.RefObject + setCameraTriggerKey: React.Dispatch> } const FloorPicker: React.FC = ({ floors, showAllFloors, toggleShowAllFloors, - mapRef, + setCameraTriggerKey, }): JSX.Element => { - const colors = useTheme().colors as Colors - const { currentFloor, setCurrentFloor, setLocation } = - useContext(MapContext) - const { t } = useTranslation('common') - const locationAlert = (): void => { - Alert.alert( - t('pages.map.details.location.title'), - t('pages.map.details.location.alert'), - [ - { - text: t('misc.cancel'), - style: 'cancel', - }, - { - text: t('pages.map.details.location.settings'), - onPress: () => { - void Linking.openSettings() - }, - }, - ] - ) - } + const theme = useTheme() + const colors = theme.colors as Colors + const isDark = theme.dark + const { currentFloor, setCurrentFloor } = useContext(MapContext) - async function getCurrentPosition(): Promise { - const { status } = await Location.requestForegroundPermissionsAsync() - if (status !== 'granted') { - setLocation('notGranted') - locationAlert() - } else { - try { - const location = await getCurrentPositionAsync({ - accuracy: Location.Accuracy.High, - }) - setLocation(location) - _injectCurrentLocation( - mapRef, - colors, - Math.min(location.coords.accuracy ?? 5, 80), - [location.coords.latitude, location.coords.longitude] - ) - _setView( - [location.coords.latitude, location.coords.longitude], - mapRef - ) - } catch (e) { - console.error(e) - } - } - } return ( @@ -89,14 +32,20 @@ const FloorPicker: React.FC = ({ { toggleShowAllFloors() - if (Platform.OS === 'ios') { - void Haptics.selectionAsync() - } }} onLongPress={() => { - setCurrentFloor('EG') - if (Platform.OS === 'ios') { - void Haptics.selectionAsync() + if (currentFloor?.floor === 'EG') { + toggleShowAllFloors() + } else { + setCurrentFloor({ floor: 'EG', manual: true }) + if ( + Platform.OS === 'ios' && + currentFloor?.floor !== 'EG' + ) { + void Haptics.impactAsync( + Haptics.ImpactFeedbackStyle.Soft + ) + } } }} > @@ -117,7 +66,9 @@ const FloorPicker: React.FC = ({ color: colors.text, }} > - {currentFloor === 'EG' ? '0' : currentFloor} + {currentFloor?.floor === 'EG' + ? '0' + : currentFloor?.floor} @@ -127,15 +78,12 @@ const FloorPicker: React.FC = ({ { - if (Platform.OS === 'ios') { - void Haptics.selectionAsync() - } toggleShowAllFloors() }} > = ({ {floors.map((floor, index) => ( { - setCurrentFloor(floor) + if (Platform.OS === 'ios') { + void Haptics.selectionAsync() + } + setCurrentFloor({ floor, manual: true }) }} key={index} > @@ -172,7 +123,7 @@ const FloorPicker: React.FC = ({ { borderBottomColor: colors.border, backgroundColor: - currentFloor === floor + currentFloor?.floor === floor ? colors.primary : colors.card, borderBottomWidth: @@ -187,7 +138,8 @@ const FloorPicker: React.FC = ({ styles.ButtonText, { color: - currentFloor === floor + currentFloor?.floor === + floor ? colors.background : colors.text, }, @@ -203,7 +155,7 @@ const FloorPicker: React.FC = ({ { { - void getCurrentPosition() + setCameraTriggerKey((prev) => prev + 1) }} > { const navigation = useNavigation() const isFocused = useNavigation().isFocused() - const [errorMsg, setErrorMsg] = useState('') - const colors = useTheme().colors as Colors + const [mapLoadState, setMapLoadState] = useState(LoadingState.LOADING) + const theme = useTheme() + const colors = theme.colors as Colors + const isDark = theme.dark + const { userKind, userFaculty } = useContext(UserKindContext) const { routeParams, updateRouteParams } = useContext(RouteParamsContext) - const [webViewKey, setWebViewKey] = useState(0) - const [loadingState, setLoadingState] = useState(LoadingState.LOADING) const [mapCenter, setMapCenter] = useState(INGOLSTADT_CENTER) const { t } = useTranslation('common') const bottomSheetRef = useRef(null) const bottomSheetModalRef = useRef(null) const currentPosition = useSharedValue(0) const currentPositionModal = useSharedValue(0) - const mapRef = useRef(null) const { localSearch, clickedElement, @@ -105,8 +101,25 @@ const MapScreen = (): JSX.Element => { setCurrentFloor, setNextLecture, } = useContext(MapContext) - + const [disableFollowUser, setDisableFollowUser] = useState(false) const [showAllFloors, setShowAllFloors] = useState(false) + const mapRef = useRef(null) + const cameraRef = useRef(null) + const locationRef = useRef(null) + const currentDate = new Date() + + enum Locations { + IN = 'Ingolstadt', + ND = 'Neuburg', + } + const lightStyle = 'https://maps.opheys.dev/styles/light/style.json' + const darkStyle = 'https://maps.opheys.dev/styles/dark/style.json' + + type LocationsType = Record + const locations: LocationsType = Locations + const [isVisible, setIsVisible] = useState(true) + const opacity = useSharedValue(1) + void MapLibreGL.setAccessToken(null) const toggleShowAllFloors = (): void => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) @@ -120,34 +133,43 @@ const MapScreen = (): JSX.Element => { return { transform: [{ translateY: bottom }], + opacity: opacity.value, } }) - const handleSheetChangesModal = useCallback((index: number) => { - if (index === -1) { - setClickedElement(null) - _setView(mapCenter, mapRef) - _removeMarker(mapRef) - bottomSheetRef.current?.snapToIndex(1) - } + const handleSheetChangesModal = useCallback(() => { + setClickedElement(null) + + bottomSheetRef.current?.snapToIndex(1) }, []) const handlePresentModalPress = useCallback(() => { bottomSheetRef.current?.close() + bottomSheetModalRef.current?.present() }, []) - const { - data: mapOverlay, - refetch, - error: overlayError, - } = useQuery({ - queryKey: ['mapOverlay', packageInfo.version], - queryFn: async () => await NeulandAPI.getMapOverlay(), - staleTime: 1000 * 60 * 60 * 24 * 7, // 1 week - gcTime: 1000 * 60 * 60 * 24 * 90, // 90 days - networkMode: 'always', - }) + const { data: mapOverlay, error: overlayError } = + useQuery({ + queryKey: ['mapOverlay', packageInfo.version], + queryFn: async () => await NeulandAPI.getMapOverlay(), + staleTime: 1000 * 60 * 60 * 24 * 7, // 1 week + gcTime: 1000 * 60 * 60 * 24 * 90, // 90 days + networkMode: 'always', + }) + + useEffect(() => { + if (overlayError != null) { + Toast.show(t('toast.mapOverlay', { ns: 'common' }), { + duration: Toast.durations.SHORT, + position: 50, + shadow: false, + animation: true, + hideOnPress: true, + delay: 0, + }) + } + }, [overlayError]) const { data: timetable } = useQuery({ queryKey: ['timetable', userKind], @@ -209,17 +231,19 @@ const MapScreen = (): JSX.Element => { } }, [timetable]) - const allRooms = useMemo(() => { + const allRooms: FeatureCollection = useMemo(() => { if (mapOverlay == null) { - return [] + console.log('No map overlay data') + return { type: 'FeatureCollection', features: [] } } const rooms = mapOverlay.features .map((feature) => { - const { geometry, properties } = feature + const { type, id, geometry, properties } = feature if ( - geometry?.coordinates == null || + geometry == null || + properties == null || geometry.type !== 'Polygon' ) { return [] @@ -228,21 +252,22 @@ const MapScreen = (): JSX.Element => { if (properties.Ebene in FLOOR_SUBSTITUTES) { properties.Ebene = FLOOR_SUBSTITUTES[properties.Ebene] } - if (!FLOOR_ORDER.includes(properties.Ebene)) { - FLOOR_ORDER.push(properties.Ebene) + if (!FLOOR_ORDER.includes(properties.Ebene as string)) { + FLOOR_ORDER.push(properties.Ebene as string) } - - return geometry.coordinates.map((points: number[][]) => ({ - properties, - coordinates: points, - options: { - type: SEARCH_TYPES.ROOM, - center: getCenterSingle(points), + return { + type, + id, + properties: { + ...properties, + rtype: SEARCH_TYPES.ROOM, + center: getCenterSingle(geometry.coordinates), icon: getIcon(SEARCH_TYPES.ROOM, { result: { item: { properties } }, }), - }, - })) + } as unknown as FeatureProperties, + geometry, + } }) .flat() const buildings = BUILDINGS.map((building) => { @@ -253,8 +278,12 @@ const MapScreen = (): JSX.Element => { new Set(buildingRooms.map((room) => room.properties.Ebene)) ).length const location = buildingRooms[0].properties.Standort - + const center = getCenter( + buildingRooms.map((x) => x.geometry.coordinates) + ) return { + type: 'Feature', + id: building, properties: { Raum: building, Funktion_en: 'Building', @@ -263,16 +292,20 @@ const MapScreen = (): JSX.Element => { Ebene: 'EG', // Dummy value to not break the floor picker Etage: floorCount.toString(), Standort: location, - } satisfies FeatureProperties, - coordinates: [], - options: { - type: SEARCH_TYPES.BUILDING, - center: getCenter(buildingRooms.map((x) => x.coordinates)), + rtype: SEARCH_TYPES.BUILDING, + center, icon: getIcon(SEARCH_TYPES.BUILDING), }, - } + geometry: { + type: 'Point', + coordinates: center, + }, + } satisfies Feature }) - return [...rooms, ...buildings] + return { + type: 'FeatureCollection', + features: [...rooms, ...buildings], + } }, [mapOverlay]) useEffect(() => { @@ -280,8 +313,9 @@ const MapScreen = (): JSX.Element => { const unsubscribe = navigation.addListener('tabPress', (e) => { // if already on the map screen, reset the map without animation // if not on the map screen, reset the map with animation - _setView(mapCenter, mapRef, isFocused) + // _setView(mapCenter, mapRef, isFocused) bottomSheetModalRef.current?.close() + setView() }) return unsubscribe @@ -291,22 +325,27 @@ const MapScreen = (): JSX.Element => { if (routeParams === null || routeParams === '') { return } - setClickedElement({ - data: routeParams, - type: SEARCH_TYPES.ROOM, - }) - const room = allRooms.find((x) => x.properties.Raum === routeParams) + const room = allRooms.features.find( + (x) => x.properties?.Raum === routeParams + )?.properties + if (room == null) { void showToast(t('toast.roomNotFound')) updateRouteParams('') return } - const center = room.options.center - const etage = room?.properties.Ebene - _setView(center, mapRef) - setCurrentFloor(etage ?? 'EG') - _injectMarker(mapRef, center, colors) + + setClickedElement({ + data: routeParams, + type: SEARCH_TYPES.ROOM, + center: room.center, + manual: false, + }) + setCurrentFloor({ + floor: room.Ebene, + manual: false, + }) handlePresentModalPress() updateRouteParams('') @@ -326,7 +365,6 @@ const MapScreen = (): JSX.Element => { updateRouteParams('') } }, [localSearch]) - const currentDate = new Date() const { data: roomStatusData } = useQuery({ queryKey: ['fnreeRooms', formatISODate(currentDate)], @@ -366,25 +404,21 @@ const MapScreen = (): JSX.Element => { } setAvailableRooms(null) void load() - }, [userKind, webViewKey, roomStatusData]) - - useEffect(() => { - if (overlayError !== null) { - void refetch() - } - }, [webViewKey]) + }, [userKind, roomStatusData]) // if current floor changes hide the detail sheet and marker useEffect(() => { if (clickedElement != null) { bottomSheetModalRef.current?.close() - bottomSheetRef.current?.snapToIndex(1) } }, [currentFloor]) const uniqueEtages = Array.from( new Set( - (Array.isArray(allRooms) ? allRooms : Object.values(allRooms)) + (Array.isArray(allRooms.features) + ? allRooms.features + : Object.values(allRooms.features) + ) .map((room: any) => room.properties?.Ebene?.toString()) .filter((etage) => etage != null) ) @@ -392,39 +426,67 @@ const MapScreen = (): JSX.Element => { (a: string, b: string) => FLOOR_ORDER.indexOf(a) - FLOOR_ORDER.indexOf(b) ) + const [filteredGeoJSON, setFilteredGeoJSON] = useState() + const [availableFilteredGeoJSON, setAvailableFilteredGeoJSON] = + useState() useEffect(() => { - if (LoadingState.LOADED !== loadingState) return - // bottomSheetRef.current?.snapToIndex(1) - _removeAllGeoJson(mapRef) - _addGeoJson() - if (clickedElement != null) { - _updateMarkerColor(mapRef, colors.primary) + if (mapOverlay == null) { + return } - }, [currentFloor, allRooms, colors, availableRooms, allRooms, loadingState]) - - const _addGeoJson = (): void => { - const filterEtage = (etage: string): RoomEntry[] => { - const result = allRooms.filter( - (feature) => feature.properties.Ebene === etage + // filter the filteredGeoJSON to only show available rooms + const filterAvailableRooms = ( + rooms: FeatureCollection | undefined + ): Array> => { + if (rooms == null) { + return [] + } + const result = rooms.features.filter( + (feature) => + feature.properties != null && + availableRooms?.find( + (x) => x.room === feature.properties?.Raum + ) ) return result } - const filteredFeatures = filterEtage(currentFloor) - filteredFeatures?.forEach((feature) => { - _addRoom(feature, availableRooms, mapRef, colors) - }) - } + const filteredFeatures = filterAvailableRooms(filteredGeoJSON) + + const availableFilteredGeoJSON: FeatureCollection< + Geometry, + GeoJsonProperties + > = { + type: 'FeatureCollection', + features: filteredFeatures, + } - const onContentProcessDidTerminate = (): void => { - setWebViewKey((k) => k + 1) - _addGeoJson() - } + setAvailableFilteredGeoJSON(availableFilteredGeoJSON) + }, [availableRooms, filteredGeoJSON, mapOverlay]) + + useEffect(() => { + if (mapOverlay == null) { + return + } + const filterEtage = (etage: string): any => { + return allRooms.features.filter( + (feature) => feature.properties?.Ebene === etage + ) + } + + const filteredFeatures = filterEtage(currentFloor?.floor ?? 'EG') + + const newGeoJSON: FeatureCollection = { + ...mapOverlay, + features: filteredFeatures, + } + + setFilteredGeoJSON(newGeoJSON) + }, [currentFloor, allRooms, mapOverlay]) // Ensure dependencies are correctly listed const getRoomData = (room: string): any => { const occupancies = availableRooms?.find((x) => x.room === room) - const properties = allRooms.find( - (x) => x.properties.Raum === room + const properties = allRooms.features.find( + (x) => x.properties?.Raum === room )?.properties const roomData = { @@ -446,18 +508,18 @@ const MapScreen = (): JSX.Element => { } const getBuildingData = (building: string): any => { - const buildingDetails = allRooms.find( + const buildingDetails = allRooms.features.find( (x) => - x.properties.Gebaeude === building && - x.options.type === SEARCH_TYPES.BUILDING + x.properties?.Gebaeude === building && + x.properties?.rtype === SEARCH_TYPES.BUILDING ) const numberOfFreeRooms = availableRooms?.filter( (x) => x.room[0] === building || x.room.slice(0, 1) === building ).length - const numberOfRooms = allRooms.filter( + const numberOfRooms = allRooms.features.filter( (x) => - x.properties.Gebaeude === building && - x.options.type === SEARCH_TYPES.ROOM + x.properties?.Gebaeude === building && + x.properties?.rtype === SEARCH_TYPES.ROOM ).length const buildingData = { title: building, @@ -472,14 +534,6 @@ const MapScreen = (): JSX.Element => { return buildingData } - enum Locations { - IN = 'Ingolstadt', - ND = 'Neuburg', - } - - type LocationsType = Record - const locations: LocationsType = Locations - const roomData: RoomData = useMemo(() => { switch (clickedElement?.type) { case SEARCH_TYPES.ROOM: @@ -496,103 +550,276 @@ const MapScreen = (): JSX.Element => { } }, [clickedElement]) + function setView(clickedElement: any = null): void { + if (clickedElement == null) { + cameraRef.current?.setCamera({ + centerCoordinate: mapCenter, + zoomLevel: 16.5, + animationDuration: 400, + heading: 0, + }) + return + } + + const [longitude, latitude] = clickedElement.center + const adjustedLatitude = latitude - 0.0003 + // Use the adjusted center for flyTo + cameraRef.current?.setCamera({ + centerCoordinate: [longitude, adjustedLatitude], + zoomLevel: 17, + animationDuration: 500, + }) + } + + const [cameraTriggerKey, setCameraTriggerKey] = useState(0) + + useEffect(() => { + if (mapOverlay == null) { + return + } + if (clickedElement == null) { + if (currentFloor?.manual !== true) { + setCurrentFloor({ floor: 'EG', manual: false }) + } + cameraRef.current?.setCamera({ + centerCoordinate: mapCenter, + zoomLevel: 16.5, + animationDuration: 500, + heading: 0, + }) + } else if (clickedElement !== null && clickedElement.manual === false) { + setView(clickedElement) + } + }, [clickedElement]) + + useFocusEffect(() => { + StatusBar.setBarStyle(theme.dark ? 'light-content' : 'dark-content') + return () => { + StatusBar.setBarStyle('default') + } + }) + + useEffect(() => { + setDisableFollowUser(false) + }, [cameraTriggerKey]) + + useEffect(() => { + if (clickedElement !== null) { + setDisableFollowUser(true) + } + }, [clickedElement]) + + const layerStyles = { + allRooms: { + fillAntialias: true, + fillColor: isDark ? '#6a7178' : '#a4a4a4', + fillOpacity: 0.1, + }, + allRoomsOutline: { + lineColor: isDark ? '#2d3035' : '#979797', + lineWidth: 2.3, + }, + availableRooms: { + fillAntialias: true, + fillOpacity: 0.2, + }, + availableRoomsOutline: { + lineWidth: 2.3, + }, + osmBackground: { + backgroundColor: isDark + ? 'rgba(166, 173, 181, 0.69)' + : 'rgba(222, 221, 203, 0.69)', + paddingHorizontal: 4, + borderRadius: 4, + }, + } + + const [regionChange, setRegionChange] = useState(false) + + useEffect(() => { + // As required by the OSM attribution, the attribution must be displayed until the user interacts with the map or 5 seconds after the map has loaded + let timer: NodeJS.Timeout + const startFadeOut = (): void => { + opacity.value = withTiming(0, { duration: 500 }, () => { + runOnJS(setIsVisible)(false) + }) + } + + if (regionChange) { + // If region changes, fade out directly without waiting for 5 seconds + startFadeOut() + } else if (!regionChange && isVisible) { + // Otherwise, wait for 5 seconds before fading out + timer = setTimeout(() => { + startFadeOut() + }, 5000) + } + + return () => { + clearTimeout(timer) + } + }, [regionChange, isVisible, opacity]) + return ( - - {loadingState === LoadingState.ERROR || - (overlayError !== null && ( - - { - setWebViewKey(webViewKey + 1) - setLoadingState(LoadingState.LOADING) - }} - /> - - ))} + <> + {mapLoadState === LoadingState.ERROR && ( + + mapRef.current?.render()} + refreshing={true} + /> + + )} + {mapLoadState === LoadingState.LOADING && ( + + + + )} + + - { + setMapLoadState(LoadingState.ERROR) + }} + onDidFinishLoadingMap={() => { + setMapLoadState(LoadingState.LOADED) + }} ref={mapRef} - source={{ - html: htmlScript, + onDidFinishRenderingMapFully={() => { + setRegionChange(false) }} - onLoadEnd={() => { - if (loadingState !== LoadingState.ERROR) { - console.log('Map loaded') - setLoadingState(LoadingState.LOADED) - _setView(mapCenter, mapRef) - _addGeoJson() - } + onRegionIsChanging={() => { + setRegionChange(true) }} - startInLoadingState={true} - renderLoading={() => ( - + + + + {filteredGeoJSON != null && ( + { + setClickedElement({ + data: e.features[0].properties?.Raum, + type: SEARCH_TYPES.ROOM, + center: e.features[0].properties + ?.center, + manual: true, + }) + + handlePresentModalPress() }} + hitbox={{ width: 0, height: 0 }} > - + - + )} - onContentProcessDidTerminate={ - onContentProcessDidTerminate - } - onMessage={(event) => { - let data = event.nativeEvent.data as any - - if (typeof data === 'string') { - try { - data = JSON.parse(data) - } catch (e) { - console.error('Could not parse data:', e) - } - } - - if (data === 'noInternetConnection') { - setLoadingState(LoadingState.ERROR) - setErrorMsg(data as string) - } else if (data.type === 'roomClick') { - setClickedElement({ - data: data.payload.properties.room, - type: SEARCH_TYPES.ROOM, - }) - const center = data.payload.properties - .center as number[] - _injectMarker(mapRef, center, colors) - Keyboard.dismiss() - bottomSheetRef.current?.close() - handlePresentModalPress() - } else { - console.log('Unhandled message:', data) - } - }} - style={{ - backgroundColor: colors.background, - }} - > - {loadingState === LoadingState.LOADED && ( - <> + {availableFilteredGeoJSON != null && ( + + + + + )} + {clickedElement != null && ( + + + + + + )} + + <> + {overlayError === null && ( - - )} + )} + + { @@ -600,17 +827,17 @@ const MapScreen = (): JSX.Element => { 'https://www.openstreetmap.org/copyright' ) }} - style={styles.osmBackground} + style={layerStyles.osmBackground} > {'© OpenStreetMap'} + @@ -635,47 +862,24 @@ const styles = StyleSheet.create({ innerContainer: { flex: 1 }, map: { flex: 1, - position: 'relative', }, errorContainer: { + flex: 1, + justifyContent: 'center', + zIndex: 100, + width: '100%', + height: '100%', position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - zIndex: 3, - paddingTop: 70, - alignItems: 'center', - }, - loadingContainer: { - top: 0, - bottom: 0, - left: 0, - position: 'absolute', - right: 0, - zIndex: 2, - }, - loadingIndicator: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - zIndex: 3, }, - osmAtrribution: { fontSize: 12 }, + osmAtrribution: { fontSize: 13 }, osmContainer: { height: 30, right: 0, - top: -17, + top: -19, marginRight: 4, alignItems: 'flex-end', - zIndex: 100, + zIndex: 99, position: 'absolute', - }, - osmBackground: { - backgroundColor: 'rgba(222, 221, 203, 0.69)', - paddingHorizontal: 4, - borderRadius: 4, + gap: 2, }, }) diff --git a/src/components/Elements/Map/ModalSections.ts b/src/components/Elements/Map/ModalSections.ts index fe2c51ab..24c9e4bb 100644 --- a/src/components/Elements/Map/ModalSections.ts +++ b/src/components/Elements/Map/ModalSections.ts @@ -87,26 +87,27 @@ export const modalSection = ( { title: t('pages.map.details.room.building'), value: - roomData.properties.Gebaeude ?? + roomData?.properties?.Gebaeude ?? t('misc.unknown'), }, { title: t('pages.map.details.room.floor'), value: - roomData.properties.Ebene ?? + roomData?.properties?.Ebene ?? t('misc.unknown'), }, { title: t('pages.map.details.room.type'), value: - roomData.properties.Funktion_en ?? + roomData?.properties?.Funktion_en ?? t('misc.unknown'), }, { title: 'Campus', value: - locations[roomData.properties.Standort] ?? - t('misc.unknown'), + locations[ + roomData?.properties?.Standort + ] ?? t('misc.unknown'), }, ], }, @@ -126,15 +127,19 @@ export const modalSection = ( items: [ { title: t('pages.map.details.building.total'), - value: occupancies.total, + value: + occupancies.total.toString() ?? t('misc.unknown'), }, { title: t('pages.map.details.building.free'), - value: occupancies.available ?? t('misc.unknown'), + value: + occupancies.available.toString() ?? + t('misc.unknown'), }, { title: t('pages.map.details.building.floors'), - value: properties?.Etage ?? t('misc.unknown'), + value: + properties?.Etage.toString() ?? t('misc.unknown'), }, { title: 'Campus', diff --git a/src/components/Elements/Map/SearchResultRow.tsx b/src/components/Elements/Map/SearchResultRow.tsx index ff8463d6..c85f0841 100644 --- a/src/components/Elements/Map/SearchResultRow.tsx +++ b/src/components/Elements/Map/SearchResultRow.tsx @@ -5,45 +5,48 @@ import { getContrastColor } from '@/utils/ui-utils' import { TouchableOpacity } from '@gorhom/bottom-sheet' import React, { useContext } from 'react' import { Keyboard, StyleSheet, Text, View } from 'react-native' -import type WebView from 'react-native-webview' import Divider from '../Universal/Divider' import PlatformIcon from '../Universal/Icon' -import { _injectMarker, _setView } from './leaflet' const ResultRow: React.FC<{ result: SearchResult index: number colors: Colors - mapRef: React.RefObject handlePresentModalPress: () => void bottomSheetRef: React.RefObject }> = ({ result, index, colors, - mapRef, handlePresentModalPress, bottomSheetRef, }): JSX.Element => { const { setClickedElement, setLocalSearch, setCurrentFloor } = useContext(MapContext) + console.log(result) return ( { - const center = result.item.options.center + const center = result.item.properties?.center Keyboard.dismiss() bottomSheetRef.current?.collapse() - _setView(center, mapRef) + // _setView(center, mapRef) setClickedElement({ data: result.title, - type: result.item.options.type, + type: result.item.properties?.rtype, + center, + manual: false, + }) + setCurrentFloor({ + floor: + (result.item.properties?.Ebene as string) ?? 'EG', + manual: false, }) - setCurrentFloor(result.item.properties.Ebene) handlePresentModalPress() - _injectMarker(mapRef, center, colors) + // _injectMarker(mapRef, center, colors) setLocalSearch('') }} > @@ -56,11 +59,11 @@ const ResultRow: React.FC<{ , - colors: Colors -): void => { - const coordinates = room.coordinates - const name = room?.properties?.Raum - const avail = availableRooms?.find((x) => x.room === name) - const color = avail != null ? colors.primary : 'grey' - - const geojsonFeature = { - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [coordinates], - }, - properties: { - room: room?.properties.Raum, - center: room?.options?.center, - }, - } - - if (mapRef.current == null) return - mapRef.current.injectJavaScript(` - var geojsonFeature = ${JSON.stringify(geojsonFeature)}; - - var layer = L.geoJSON(geojsonFeature, { - color: ${JSON.stringify(color)}, - fillOpacity: 0.2, - }).addTo(mymap).bringToBack(); - - // Add click event listener to the layer - layer.on('click', function(e) { - var properties = e.layer.feature.properties; - - - // Send data to React Native app - sendMessageToRN(JSON.stringify({ - type: 'roomClick', - payload: { - type: 'room', - properties: properties, - }, - })); - }); - true; - `) -} - -/** - * Removes all GeoJSON layers from the Leaflet map. - * - * @param mapRef - A reference to the Leaflet map. - * @returns void - */ -export const _removeAllGeoJson = (mapRef: React.RefObject): void => { - mapRef.current?.injectJavaScript(` - mymap.eachLayer(function (layer) { - if (layer instanceof L.GeoJSON) { - mymap.removeLayer(layer); - } - }); - true - `) -} - -/** - * Sets the center of the Leaflet map to the specified coordinates. - * - * @param center - The coordinates to center the map on. - * @param mapRef - A reference to the Leaflet map. - * @param animate - Whether to animate the transition. - * @returns void - */ -export const _setView = ( - center: number[] | undefined, - mapRef: React.RefObject, - animate: boolean = true -): void => { - if (center == null) return - mapRef.current?.injectJavaScript(` -mymap.setView(${JSON.stringify(center)}, 17.5, { animate: ${animate} }); -true; -`) -} - -// inject the current location into the map -export const _injectCurrentLocation = ( - mapRef: React.RefObject, - colors: Colors, - accuracy: number, - currentLocation: number[] -): void => { - mapRef.current?.injectJavaScript(` -if (window.currentLocationMarker) { - window.currentLocationMarker.remove(); -} - -window.currentLocationMarker = L.circle(${JSON.stringify(currentLocation)}, { - color: ${JSON.stringify(colors.primary)}, - fillColor: ${JSON.stringify(colors.primary)}, - fillOpacity: 0.6, - radius: ${accuracy}, -}).addTo(mymap); -true; -`) -} - -export const _injectMarker = ( - mapRef: React.RefObject, - coordinates: number[], - colors: Colors -): void => { - const color = colors.primary - const svg = ` - - - - -` - const base64Svg = Buffer.from(svg).toString('base64') - mapRef.current?.injectJavaScript(` -if (window.marker) { - window.marker.remove(); -} - -window.marker = L.marker(${JSON.stringify(coordinates)}, { - icon: L.icon({ - iconUrl: 'data:image/svg+xml;base64,${base64Svg}', - iconSize: [45, 45], - iconAnchor: [24, 42], - }) -}).addTo(mymap); - -true; -`) -} - -// update marker color wihtout removing it -export const _updateMarkerColor = ( - mapRef: React.RefObject, - color: string -): void => { - mapRef.current?.injectJavaScript(` -if (window.marker) { - window.marker.setIcon(L.icon({ - iconUrl: 'data:image/svg+xml;base64,${Buffer.from( - ` - - - - ` - ).toString('base64')}', - iconAnchor: [24, 42], - iconSize: [45, 45], - })); -} -true; -`) -} - -// remove the marker from the map -export const _removeMarker = (mapRef: React.RefObject): void => { - mapRef.current?.injectJavaScript(` -if (window.marker) { - window.marker.remove(); -} -true; -`) -} - -/** - * A string containing an HTML script that initializes a Leaflet map with OpenStreetMap tiles and event listeners for error and internet connection checking. - */ -export const htmlScript = ` - - - - - Quick Start - Leaflet - - - - - - - - -

- - - - - - -` diff --git a/src/data/calendar.json b/src/data/calendar.json index aa169760..e6d6b85a 100644 --- a/src/data/calendar.json +++ b/src/data/calendar.json @@ -1,9 +1,6 @@ [ { - "name": { - "de": "", - "en": "" - }, + "name": "", "begin": "1970-01-01", "comments": [ "If you work on this file, please triple-check if you entered all days correctly.", @@ -13,94 +10,144 @@ }, { "name": { - "de": "Semesterferien", - "en": "Semester break" + "de": "Rückmeldung zum Wintersemester 2024/2025", + "en": "Re-registration for winter semester 2024/2025" }, - "begin": "2023-08-01", - "end": "2023-09-30" + "begin": "2024-07-02", + "end": "2024-08-07" }, { "name": { - "de": "Vorlesungsbeginn und Semesterstart", - "en": "Start of lectures and semester" + "de": "Erweiterter Prüfungszeitraum", + "en": "Additional exam period" }, - "begin": "2023-10-02" + "begin": "2024-07-04", + "end": "2024-07-10" }, { "name": { - "de": "Vorlesungsfrei", - "en": "No lectures" + "de": "Ende der Vorlesungszeit", + "en": "End of lecture period" + }, + "begin": "2024-07-10" + }, + { + "name": { + "de": "Prüfungszeitraum", + "en": "Exam period" + }, + "begin": "2024-07-11", + "end": "2024-07-23" + }, + { + "name": { + "de": "Notenmeldung", + "en": "Recording grades" + }, + "begin": "2024-07-26T9:00", + "hasHours": true + }, + + { + "name": { + "de": "Notenbekanntgabe", + "en": "Announcement of grades" }, - "begin": "2023-10-03" + "begin": "2024-07-31T13:00", + "hasHours": true + }, + { + "name": { + "de": "Semesterferien", + "en": "Semester break" + }, + "begin": "2024-08-01", + "end": "2024-09-30" + }, + { + "name": { + "de": "Vorlesungsbeginn und Semesterstart", + "en": "Start of lectures and semester" + }, + "begin": "2024-10-01" }, { "name": { "de": "Vorlesungsfrei", "en": "No lectures" }, - "begin": "2023-10-31" + "begin": "2024-10-31", + "end": "2024-11-01" }, { "name": { "de": "Prüfungsanmeldung", "en": "Registration for exams" }, - "begin": "2023-11-06", - "end": "2023-11-16" + "begin": "2024-11-08", + "end": "2024-11-17" }, { "name": { "de": "Prüfungsabmeldung", "en": "Deregistration for exams" }, - "begin": "2023-11-06", - "end": "2024-01-11" + "begin": "2024-11-08", + "end": "2025-01-10" }, { "name": { "de": "Vorlesungsfrei (Weihnachten)", "en": "No lectures (Christmas)" }, - "begin": "2023-12-24", - "end": "2024-01-07" + "begin": "2024-12-21", + "end": "2025-01-06" }, { "name": { "de": "Notenmeldung Prädikate (ZV)", "en": "Recording grades predicates (admission requirements)" }, - "begin": "2024-01-09T9:00", + "begin": "2025-01-08T9:00", "hasHours": true }, + { + "name": { + "de": "Rückmeldung zum Sommersemester 2025", + "en": "Re-registration for summer semester 2025" + }, + "begin": "2025-01-14", + "end": "2025-01-31" + }, { "name": { "de": "Erweiterter Prüfungszeitraum", "en": "Additional exam period" }, - "begin": "2024-01-19", - "end": "2024-01-25" + "begin": "2025-01-18", + "end": "2025-01-24" }, { "name": { "de": "Ende der Vorlesungszeit", "en": "End of lecture period" }, - "begin": "2024-01-25" + "begin": "2025-01-24" }, { "name": { "de": "Prüfungszeitraum", "en": "Exam period" }, - "begin": "2024-01-26", - "end": "2024-02-05" + "begin": "2025-01-25", + "end": "2025-02-04" }, { "name": { "de": "Notenmeldung", "en": "Recording grades" }, - "begin": "2024-02-09T9:00", + "begin": "2025-02-10T9:00", "hasHours": true }, { @@ -108,7 +155,7 @@ "de": "Notenbekanntgabe", "en": "Announcement of grades" }, - "begin": "2024-02-14T13:00", + "begin": "2025-02-14T13:00", "hasHours": true }, { @@ -116,100 +163,101 @@ "de": "Semesterferien", "en": "Semester break" }, - "begin": "2024-02-15", - "end": "2024-03-14" + "begin": "2025-02-15", + "end": "2025-03-14" }, { "name": { "de": "Vorlesungsbeginn und Semesterstart", "en": "Start of lectures and semester" }, - "begin": "2024-03-18" + "begin": "2025-03-17" }, { "name": { "de": "Vorlesungsfrei (Ostern)", "en": "No lectures (Easter)" }, - "begin": "2024-03-28", - "end": "2024-04-02" + "begin": "2025-04-17", + "end": "2025-04-22" }, { "name": { "de": "Prüfungsanmeldung", "en": "Registration for exams" }, - "begin": "2024-04-23", - "end": "2024-05-02" + "begin": "2025-04-30", + "end": "2025-05-09" }, { "name": { "de": "Prüfungsabmeldung", "en": "Deregistration for exams" }, - "begin": "2024-04-23", - "end": "2024-06-26" - }, - { - "name": { - "de": "Vorlesungsfrei (Christi Himmelfahrt, Brückentag)", - "en": "No lectures (Ascension Day, Bridge Day)" - }, - "begin": "2024-05-09", - "end": "2024-05-10" + "begin": "2025-04-30", + "end": "2025-06-26" }, { "name": { "de": "Vorlesungsfrei (Pfingsten)", "en": "No lectures (Whitsun)" }, - "begin": "2024-05-17", - "end": "2024-05-21" + "begin": "2025-06-06", + "end": "2025-06-10" }, { "name": { - "de": "Vorlesungsfrei (Fronleichnam)", - "en": "No lectures (Corpus Christi)" + "de": "Vorlesungsfrei (Fronleichnam, Brückentag)", + "en": "No lectures (Corpus Christi, Bridge Day)" }, - "begin": "2024-05-30" + "begin": "2025-06-19", + "end": "2025-06-20" }, { "name": { "de": "Notenmeldung Prädikate (ZV)", "en": "Recording grades predicates (admission requirements)" }, - "begin": "2024-06-19T9:00", + "begin": "2025-06-18T9:00", "hasHours": true }, + { + "name": { + "de": "Rückmeldung zum Wintersemester 2025/2026", + "en": "Re-registration for winter semester 2025/2026" + }, + "begin": "2025-07-02", + "end": "2025-08-07" + }, { "name": { "de": "Erweiterter Prüfungszeitraum", "en": "Additional exam period" }, - "begin": "2024-07-04", - "end": "2024-07-10" + "begin": "2025-07-04", + "end": "2025-07-10" }, { "name": { "de": "Ende der Vorlesungszeit", "en": "End of lecture period" }, - "begin": "2024-07-10" + "begin": "2025-07-10" }, { "name": { "de": "Prüfungszeitraum", "en": "Exam period" }, - "begin": "2024-07-11", - "end": "2024-07-23" + "begin": "2025-07-11", + "end": "2025-07-23" }, { "name": { "de": "Notenmeldung", "en": "Recording grades" }, - "begin": "2024-07-26T9:00", + "begin": "2025-07-28T9:00", "hasHours": true }, { @@ -217,7 +265,7 @@ "de": "Notenbekanntgabe", "en": "Announcement of grades" }, - "begin": "2024-07-31T13:00", + "begin": "2025-07-31T13:00", "hasHours": true }, { @@ -225,7 +273,7 @@ "de": "Semesterferien", "en": "Semester break" }, - "begin": "2024-08-01", - "end": "2024-09-30" + "begin": "2025-08-01", + "end": "2025-09-30" } ] diff --git a/src/data/changelog.json b/src/data/changelog.json index 637c7b7f..85c830b0 100644 --- a/src/data/changelog.json +++ b/src/data/changelog.json @@ -236,6 +236,51 @@ "android": "bug_report" } } + ], + "0.8": [ + { + "title": { + "de": "Karten Darkmode", + "en": "Map Darkmode" + }, + "description": { + "de": "Aktiviere den Darkmode, um eine dunkle Kartenansicht zu erhalten. Zudem wurde auch die helle Ansicht komplett erneuert und optimiert.", + "en": "Enable dark mode to get a dark map view. In addition, the light view has also been completely renewed and optimized." + }, + "icon": { + "ios": "moon.stars", + "android": "dark_mode" + } + }, + { + "title": { + "de": "Native Karten", + "en": "Native Map" + }, + "description": { + "de": "Die Karte komplett neu geschrieben und verfügt nun über natives Rendering, Gesten und noch mehr Informationen.", + "en": "The map has been completely rewritten and now has native rendering, gestures and even more information." + }, + "icon": { + "ios": "map", + "android": "map" + } + }, + + { + "title": { + "de": "Event Details", + "en": "Event Details" + }, + "description": { + "de": "Tippe auf ein Event, um mehr Informationen zu erhalten, wie zum Ort oder der Beschreibung.", + "en": "Tap on an event to get more information, such as the location or description." + }, + "icon": { + "ios": "party.popper", + "android": "event" + } + } ] } } diff --git a/src/data/licenses-static.json b/src/data/licenses-static.json index e4504c63..deb4abf0 100644 --- a/src/data/licenses-static.json +++ b/src/data/licenses-static.json @@ -13,10 +13,10 @@ "parents": "neuland", "platform": "ios" }, - "leaftlet@1.7.4": { - "licenses": "BSD-2-Clause", - "repository": "https://github.com/Leaflet/Leaflet/", - "licenseUrl": "https://raw.githubusercontent.com/Leaflet/Leaflet/main/LICENSE", + "OpenStreetMap": { + "licenses": "ODbL", + "repository": "https://www.openstreetmap.org/", + "licenseUrl": "https://www.openstreetmap.org/copyright", "parents": "neuland", "platform": "all" } diff --git a/src/data/licenses.json b/src/data/licenses.json index 28e993f2..de77a106 100644 --- a/src/data/licenses.json +++ b/src/data/licenses.json @@ -35,6 +35,12 @@ "licenseUrl": "https://github.com/Kichiyaki/react-native-barcode-generator/raw/master/LICENSE", "parents": "neuland" }, + "@maplibre/maplibre-react-native@10.0.0-alpha.5": { + "licenses": "MIT", + "repository": "https://github.com/maplibre/maplibre-react-native", + "licenseUrl": "https://github.com/maplibre/maplibre-react-native/raw/master/LICENSE.md", + "parents": "neuland" + }, "@react-native-async-storage/async-storage@1.23.1": { "licenses": "MIT", "repository": "https://github.com/react-native-async-storage/async-storage", @@ -53,12 +59,6 @@ "licenseUrl": "https://github.com/react-native-netinfo/react-native-netinfo/raw/master/LICENSE", "parents": "neuland" }, - "@react-native-menu/menu@0.9.1": { - "licenses": "MIT", - "repository": "https://github.com/react-native-menu/menu", - "licenseUrl": "https://github.com/react-native-menu/menu/raw/master/LICENSE", - "parents": "neuland" - }, "@sentry/react-native@5.22.3": { "licenses": "MIT", "repository": "https://github.com/getsentry/sentry-react-native", @@ -77,13 +77,13 @@ "licenseUrl": "https://github.com/TanStack/query/raw/master/LICENSE", "parents": "neuland" }, - "@tanstack/react-query-persist-client@5.45.0": { + "@tanstack/react-query-persist-client@5.45.1": { "licenses": "MIT", "repository": "https://github.com/TanStack/query", "licenseUrl": "https://github.com/TanStack/query/raw/master/LICENSE", "parents": "neuland" }, - "@tanstack/react-query@5.45.0": { + "@tanstack/react-query@5.45.1": { "licenses": "MIT", "repository": "https://github.com/TanStack/query", "licenseUrl": "https://github.com/TanStack/query/raw/master/LICENSE", diff --git a/src/hooks/contexts/map.ts b/src/hooks/contexts/map.ts index c044c0c8..dd4c13ab 100644 --- a/src/hooks/contexts/map.ts +++ b/src/hooks/contexts/map.ts @@ -12,8 +12,8 @@ interface MapContextType { setAvailableRooms: (_: AvailableRoom[] | null) => void nextLecture: FriendlyTimetableEntry[] | null setNextLecture: (_: FriendlyTimetableEntry[] | null) => void - currentFloor: string - setCurrentFloor: (_: string) => void + currentFloor: { floor: string; manual: boolean } | null + setCurrentFloor: (_: { floor: string; manual: boolean }) => void location: LocationObject | null | 'notGranted' setLocation: (_: LocationObject | null | 'notGranted') => void } @@ -28,8 +28,8 @@ export const MapContext = createContext({ availableRooms: null, setAvailableRooms: (_: AvailableRoom[] | null) => {}, - currentFloor: '', - setCurrentFloor: (_: string) => {}, + currentFloor: null, + setCurrentFloor: (_: { floor: string; manual: boolean }) => {}, location: null, setLocation: (_: LocationObject | null | 'notGranted') => {}, diff --git a/src/localization/de/common.ts b/src/localization/de/common.ts index 114d894e..d0441406 100644 --- a/src/localization/de/common.ts +++ b/src/localization/de/common.ts @@ -3,6 +3,7 @@ export default { clipboard: 'in Zwischenablage kopiert', paused: 'Keine Internetverbindung', roomNotFound: 'Raum nicht gefunden', + mapOverlay: 'Fehler beim Laden des Overlays', }, error: { title: 'Ein Fehler ist aufgetreten', @@ -140,7 +141,7 @@ export default { easterEgg: { title: 'Easter Egg', message: - 'Du hast das exklusive App-Icon "Neuland Pink" freigeschaltet! 🩷', + 'Du hast das exklusive retro App-Icon "neuland.app" freigeschaltet!', confirm: 'Nice!', }, noAvailableRooms: 'Keine freien Räume verfügbar', @@ -173,6 +174,7 @@ export default { alert: 'Um deinen aktuellen Standort anzuzeigen, musst du die Standortberechtigung aktivieren.', button: 'Einstellungen', }, + osm: 'Karten Daten von OpenStreetMap', }, }, rooms: { diff --git a/src/localization/de/settings.ts b/src/localization/de/settings.ts index fd51e7d6..18a4f66b 100644 --- a/src/localization/de/settings.ts +++ b/src/localization/de/settings.ts @@ -167,7 +167,7 @@ export default { names: { default: 'Neuland Next', modernDark: 'Modern Dunkel', - modernLight: 'Modern Hell', + retro: 'neuland.app', modernGreen: 'Modern Grün', modernPink: 'Modern Pink', rainbowMoonLight: 'Regenbogen Hell', diff --git a/src/localization/en/common.ts b/src/localization/en/common.ts index c2512eb5..ec2c22a1 100644 --- a/src/localization/en/common.ts +++ b/src/localization/en/common.ts @@ -3,6 +3,7 @@ export default { clipboard: 'copied to clipboard', paused: 'No internet connection', roomNotFound: 'Room not found', + mapOverlay: 'Error while loading overlay', }, error: { title: 'An error occurred', @@ -138,7 +139,7 @@ export default { easterEgg: { title: 'Easter Egg', message: - 'You unlocked the exclusive app icon "Neuland Pink"! 🩷', + 'You unlocked the exclusive retro app icon "neuland.app"!', confirm: 'Nice!', }, noAvailableRooms: 'No free rooms available', @@ -172,6 +173,7 @@ export default { alert: 'To see your current location, please enable location services.', settings: 'Settings', }, + osm: 'Map data from OpenStreetMap', }, }, diff --git a/src/localization/en/settings.ts b/src/localization/en/settings.ts index 1557d119..62fe1524 100644 --- a/src/localization/en/settings.ts +++ b/src/localization/en/settings.ts @@ -164,7 +164,7 @@ export default { names: { default: 'Neuland Next', modernDark: 'Modern Dark', - modernLight: 'Modern Light', + retro: 'neuland.app', modernGreen: 'Modern Green', modernPink: 'Modern Pink', rainbowMoonLight: 'Rainbow Light', diff --git a/src/types/asset-api.ts b/src/types/asset-api.ts index d5a24764..7d9399ca 100644 --- a/src/types/asset-api.ts +++ b/src/types/asset-api.ts @@ -1,3 +1,5 @@ +import { type MaterialIcon } from './material-icons' + export interface RoomsOverlay { type: string features: Feature[] @@ -42,6 +44,9 @@ export interface FeatureProperties { Raum: string Funktion_de: string Funktion_en: string + rtype?: number + center?: number[] + icon?: { ios: string; android: MaterialIcon } } export enum Gebaeude { diff --git a/src/types/map.ts b/src/types/map.ts index 51c55d3a..59cb3bad 100644 --- a/src/types/map.ts +++ b/src/types/map.ts @@ -1,4 +1,11 @@ -import { type AvailableRoom, type RoomEntry } from './utils' +import { + type Feature, + type GeoJsonProperties, + type Geometry, + type Position, +} from 'geojson' + +import { type AvailableRoom } from './utils' export enum SEARCH_TYPES { BUILDING, @@ -8,7 +15,7 @@ export enum SEARCH_TYPES { export interface RoomData { title: string subtitle: string - properties: RoomEntry['properties'] + properties: GeoJsonProperties occupancies: AvailableRoom | BuildingOccupancy type: SEARCH_TYPES } @@ -20,19 +27,13 @@ export interface BuildingOccupancy { export interface ClickedMapElement { type: SEARCH_TYPES data: string -} - -export interface searchResult { - type: SEARCH_TYPES - highlight: RoomEntry[] - title: string - subtitle: string - center: number[] + center?: Position + manual?: boolean } export interface SearchResult { title: string subtitle: string isExactMatch?: boolean - item: RoomEntry + item: Feature } diff --git a/src/types/utils.ts b/src/types/utils.ts index 5414e872..b068e5ae 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -1,5 +1,3 @@ -import { type SEARCH_TYPES } from './map' -import { type MaterialIcon } from './material-icons' import { type Lecturers } from './thi-api' export interface Exam { @@ -58,31 +56,6 @@ export interface AvailableRoom { capacity: number } -export interface RoomEntry { - coordinates: number[][] - options: RoomOptions - properties: Properties -} - -interface RoomOptions { - center: number[] - type: SEARCH_TYPES - icon: { - ios: string - android: MaterialIcon - } -} - -interface Properties { - Ebene: string - Etage: string - Funktion_de: string - Funktion_en: string - Gebaeude: string - Raum: string - Standort: string -} - export interface FriendlyTimetableEntry { date: Date startDate: Date diff --git a/src/utils/map-utils.ts b/src/utils/map-utils.ts index 8d0ff561..373461c7 100644 --- a/src/utils/map-utils.ts +++ b/src/utils/map-utils.ts @@ -1,9 +1,9 @@ -import { type FeatureProperties } from '@/types/asset-api' import { SEARCH_TYPES } from '@/types/map' import { type MaterialIcon } from '@/types/material-icons' import { type Rooms } from '@/types/thi-api' import { type AvailableRoom } from '@/types/utils' import { trackEvent } from '@aptabase/react-native' +import { type GeoJsonProperties, type Position } from 'geojson' import { type TFunction } from 'i18next' import { Platform, Share } from 'react-native' @@ -29,8 +29,8 @@ export const BUILDINGS_IN = [ 'X', 'Z', ] -export const INGOLSTADT_CENTER = [48.7667, 11.4328] -export const NEUBURG_CENTER = [48.73227, 11.17261] +export const INGOLSTADT_CENTER = [11.4328, 48.7663] +export const NEUBURG_CENTER = [11.17261, 48.732] export const BUILDINGS_ND = ['BN', 'CN'] export const BUILDINGS = [...BUILDINGS_IN, ...BUILDINGS_ND] export const BUILDINGS_ALL = 'Alle' @@ -145,7 +145,6 @@ export function getRoomOpenings(rooms: Rooms[], date: Date): RoomOpenings { }) ) ) - // iterate over every room .forEach( ({ room, @@ -274,20 +273,20 @@ export async function searchRooms( .sort((a, b) => a.room.localeCompare(b.room)) } -export function getCenter(rooms: number[][][]): number[] { - const getCenterPoint = (points: number[][]): number[] => { - const x = points.map((point) => point[0]) - const y = points.map((point) => point[1]) - const minX = Math.min(...x) - const maxX = Math.max(...x) - const minY = Math.min(...y) - const maxY = Math.max(...y) - return [(minX + maxX) / 2, (minY + maxY) / 2] +export function getCenter(rooms: Position[][][]): Position { + const getCenterPoint = (points: Position[][]): Position[] => { + const x = points[0].map((point: any) => point[0]) + const y = points[0].map((point: any) => point[1]) + const minX = Math.min(...(x as number[])) + const maxX = Math.max(...(x as number[])) + const minY = Math.min(...(y as number[])) + const maxY = Math.max(...(y as number[])) + return [(minX + maxX) / 2, (minY + maxY) / 2] as unknown as Position[] } const centerPoints = rooms.reduce( (acc, room) => { - const centerPoint = getCenterPoint(room) + const centerPoint = getCenterPoint(room) as unknown as number[] acc.lon += centerPoint[0] acc.lat += centerPoint[1] acc.count += 1 @@ -297,16 +296,18 @@ export function getCenter(rooms: number[][][]): number[] { ) return [ - centerPoints.lat / centerPoints.count, centerPoints.lon / centerPoints.count, + centerPoints.lat / centerPoints.count, ] } -export function getCenterSingle(coordinates: number[][] | undefined): number[] { +export function getCenterSingle( + coordinates: number[][][] | undefined +): number[] { if (coordinates == null) { return INGOLSTADT_CENTER } - const centerPoints = coordinates.reduce( + const centerPoints = coordinates[0].reduce( (acc, coordinate) => { acc.lon += coordinate[0] acc.lat += coordinate[1] @@ -317,8 +318,8 @@ export function getCenterSingle(coordinates: number[][] | undefined): number[] { ) return [ - centerPoints.lat / centerPoints.count, centerPoints.lon / centerPoints.count, + centerPoints.lat / centerPoints.count, ] } @@ -380,13 +381,16 @@ export const determineSearchType = (search: string): SEARCH_TYPES => { export const getIcon = ( type: SEARCH_TYPES, - properties?: { result: { item: { properties: FeatureProperties } } } + properties?: { result: { item: { properties: GeoJsonProperties } } } ): { ios: string; android: MaterialIcon } => { const { Funktion_en: funktionEn, Raum: raum, - }: { Funktion_en: string; Raum: string } = properties?.result.item - .properties ?? { Funktion_en: '', Raum: '' } + }: { Funktion_en: string; Raum: string } = (properties?.result.item + .properties as { Funktion_en: string; Raum: string }) ?? { + Funktion_en: '', + Raum: '', + } const food = ['M001', 'X001', 'F001'] switch (type) { case SEARCH_TYPES.BUILDING: