diff --git a/.github/workflows/gradle.yaml b/.github/workflows/gradle.yaml index b6ac3309da..504430f109 100644 --- a/.github/workflows/gradle.yaml +++ b/.github/workflows/gradle.yaml @@ -143,6 +143,51 @@ jobs: files: | ./SlimeVR-android.apk + bundle-ios: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Get tags + run: git fetch --tags origin --recurse-submodules=no --force + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: "17" + distribution: "adopt" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - uses: pnpm/action-setup@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'pnpm' + + - run: pnpm i + + - name: Build GUI + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + NODE_OPTIONS: --max-old-space-size=4096 + run: cd gui && pnpm run build + + - name: Build with Gradle + run: ./gradlew :server:ios:createIPA + + - name: Upload the iOS Build Artifact + uses: actions/upload-artifact@v4 + with: + # Artifact name + name: "SlimeVR-iOS" # optional, default is artifact + # A file, directory or wildcard pattern that describes what to upload + path: server/ios/build/robovmx-build/tmp/Main.ipa + bundle-linux: strategy: matrix: diff --git a/gradle.properties b/gradle.properties index da78dfd873..42ef338e1f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,3 +18,4 @@ spotlessVersion=7.0.2 shadowJarVersion=8.3.2 buildconfigVersion=5.5.0 grgitVersion=5.2.2 +robovmVersion=10.2.2.4-SNAPSHOT diff --git a/gui/src-tauri/icons/icon.icns b/gui/src-tauri/icons/icon.icns index 753e44c34c..3ea09b45dd 100644 Binary files a/gui/src-tauri/icons/icon.icns and b/gui/src-tauri/icons/icon.icns differ diff --git a/gui/src-tauri/icons/ios/AppIcon-20x20@1x.png b/gui/src-tauri/icons/ios/AppIcon-20x20@1x.png index 383c253527..d842573041 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-20x20@1x.png and b/gui/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/gui/src-tauri/icons/ios/AppIcon-20x20@2x-1.png index 28f720c69a..c5b356d2f8 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-20x20@2x-1.png and b/gui/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-20x20@2x.png b/gui/src-tauri/icons/ios/AppIcon-20x20@2x.png index 28f720c69a..c5b356d2f8 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-20x20@2x.png and b/gui/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-20x20@3x.png b/gui/src-tauri/icons/ios/AppIcon-20x20@3x.png index 72267593bb..3d49086535 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-20x20@3x.png and b/gui/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-29x29@1x.png b/gui/src-tauri/icons/ios/AppIcon-29x29@1x.png index 3582cf6ba7..258d802ca4 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-29x29@1x.png and b/gui/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/gui/src-tauri/icons/ios/AppIcon-29x29@2x-1.png index 26a9a81efb..37ae061bad 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-29x29@2x-1.png and b/gui/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-29x29@2x.png b/gui/src-tauri/icons/ios/AppIcon-29x29@2x.png index 26a9a81efb..37ae061bad 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-29x29@2x.png and b/gui/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-29x29@3x.png b/gui/src-tauri/icons/ios/AppIcon-29x29@3x.png index 0aa9c6842c..5d85316ce5 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-29x29@3x.png and b/gui/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-40x40@1x.png b/gui/src-tauri/icons/ios/AppIcon-40x40@1x.png index 28f720c69a..c5b356d2f8 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-40x40@1x.png and b/gui/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/gui/src-tauri/icons/ios/AppIcon-40x40@2x-1.png index 9f3286ca2f..cfc94a0e7b 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-40x40@2x-1.png and b/gui/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-40x40@2x.png b/gui/src-tauri/icons/ios/AppIcon-40x40@2x.png index 9f3286ca2f..cfc94a0e7b 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-40x40@2x.png and b/gui/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-40x40@3x.png b/gui/src-tauri/icons/ios/AppIcon-40x40@3x.png index 21f126cf16..28bb14ec3c 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-40x40@3x.png and b/gui/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-512@2x.png b/gui/src-tauri/icons/ios/AppIcon-512@2x.png index 204f8fa30a..4471a53300 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-512@2x.png and b/gui/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-60x60@2x.png b/gui/src-tauri/icons/ios/AppIcon-60x60@2x.png index 21f126cf16..28bb14ec3c 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-60x60@2x.png and b/gui/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-60x60@3x.png b/gui/src-tauri/icons/ios/AppIcon-60x60@3x.png index a8390e3161..6916323ca5 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-60x60@3x.png and b/gui/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-76x76@1x.png b/gui/src-tauri/icons/ios/AppIcon-76x76@1x.png index 5486ec449b..19e5a1f2b8 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-76x76@1x.png and b/gui/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-76x76@2x.png b/gui/src-tauri/icons/ios/AppIcon-76x76@2x.png index 84497bb0a6..548f993305 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-76x76@2x.png and b/gui/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/gui/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/gui/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png index c20b91316a..9a9640e238 100644 Binary files a/gui/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png and b/gui/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/gui/src-tauri/icons/ios/Contents.json b/gui/src-tauri/icons/ios/Contents.json new file mode 100644 index 0000000000..9664c57335 --- /dev/null +++ b/gui/src-tauri/icons/ios/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "AppIcon-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "AppIcon-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "AppIcon-20x20@2x 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-29x29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-40x40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "AppIcon-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "AppIcon-83.5x83.5@2x 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "AppIcon-512@2x 1.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "AppIcon-20x20@1x.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "20x20" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/gui/src/components/EmptyLayout.tsx b/gui/src/components/EmptyLayout.tsx index 443ea60a8c..423ed820a3 100644 --- a/gui/src/components/EmptyLayout.tsx +++ b/gui/src/components/EmptyLayout.tsx @@ -1,11 +1,15 @@ import { ReactNode } from 'react'; import { TopBar } from './TopBar'; import './EmptyLayout.scss'; +import classNames from 'classnames'; export function EmptyLayout({ children }: { children: ReactNode }) { return (
-
+
diff --git a/gui/src/components/MainLayout.tsx b/gui/src/components/MainLayout.tsx index 3cd488eaa3..a45e019e25 100644 --- a/gui/src/components/MainLayout.tsx +++ b/gui/src/components/MainLayout.tsx @@ -60,7 +60,10 @@ export function MainLayout({ return (
-
+
@@ -71,7 +74,8 @@ export function MainLayout({ className={classNames( 'overflow-y-auto mr-2 my-2 mobile:m-0', 'flex flex-col rounded-xl', - background && 'bg-background-70' + background && 'bg-background-70', + window.__IOS__ && 'mobile:mt-10' )} > {children} diff --git a/gui/src/components/TopBar.tsx b/gui/src/components/TopBar.tsx index 76b1fb183b..f408750ffb 100644 --- a/gui/src/components/TopBar.tsx +++ b/gui/src/components/TopBar.tsx @@ -232,19 +232,22 @@ export function TopBar({ )} - {!isTauri && !showVersionMobile && !config?.decorations && ( -
+ {!isTauri && + !showVersionMobile && + !config?.decorations && + !window.__IOS__ && (
- SlimeVR +
+ SlimeVR +
-
- )} + )}
- {(typeof __ANDROID__ === 'undefined' || !__ANDROID__?.isThere()) && ( - - )} + {(!window.__IOS__ || + typeof __ANDROID__ === 'undefined' || + !__ANDROID__?.isThere()) && }
diff --git a/gui/src/components/onboarding/OnboardingLayout.tsx b/gui/src/components/onboarding/OnboardingLayout.tsx index 671ada5e6d..4599035514 100644 --- a/gui/src/components/onboarding/OnboardingLayout.tsx +++ b/gui/src/components/onboarding/OnboardingLayout.tsx @@ -6,6 +6,7 @@ import { useBreakpoint } from '@/hooks/breakpoint'; import { SkipSetupButton } from './SkipSetupButton'; import { SkipSetupWarningModal } from './SkipSetupWarningModal'; import './OnboardingLayout.scss'; +import classNames from 'classnames'; export function OnboardingLayout({ children }: { children: ReactNode }) { const { isMobile } = useBreakpoint('mobile'); @@ -14,10 +15,19 @@ export function OnboardingLayout({ children }: { children: ReactNode }) { return !state.alonePage ? (
-
+
-
+
+
-
+
@@ -118,7 +127,10 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
{isMobile && } {children} diff --git a/gui/src/components/settings/pages/InterfaceSettings.tsx b/gui/src/components/settings/pages/InterfaceSettings.tsx index 7973a8f2cb..690f7c4f75 100644 --- a/gui/src/components/settings/pages/InterfaceSettings.tsx +++ b/gui/src/components/settings/pages/InterfaceSettings.tsx @@ -248,27 +248,33 @@ export function InterfaceSettings() { )} - - {l10n.getString('settings-general-interface-discord_presence')} - -
- - {l10n.getString( - 'settings-general-interface-discord_presence-description' - )} - -
-
- -
+ {isTauri() && ( + <> + + {l10n.getString( + 'settings-general-interface-discord_presence' + )} + +
+ + {l10n.getString( + 'settings-general-interface-discord_presence-description' + )} + +
+
+ +
+ + )} {l10n.getString('settings-general-interface-dev_mode')} diff --git a/gui/src/vite-env.d.ts b/gui/src/vite-env.d.ts index ef7bc23d8e..3c5f94fa5d 100644 --- a/gui/src/vite-env.d.ts +++ b/gui/src/vite-env.d.ts @@ -12,6 +12,7 @@ declare const __ANDROID__: interface Window { readonly isTauri: boolean; + readonly __IOS__: boolean | undefined; } declare module 'tailwind-gradient-mask-image'; diff --git a/gui/vite.config.ts b/gui/vite.config.ts index a09fed7b64..632b4bac12 100644 --- a/gui/vite.config.ts +++ b/gui/vite.config.ts @@ -36,6 +36,7 @@ export function i18nHotReload(): PluginOption { // https://vitejs.dev/config/ export default defineConfig({ + base: "./", define: { __COMMIT_HASH__: JSON.stringify(commitHash), __VERSION_TAG__: JSON.stringify(versionTag), diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts index 0da1661ad2..81f25380c6 100644 --- a/server/core/build.gradle.kts +++ b/server/core/build.gradle.kts @@ -64,8 +64,8 @@ dependencies { // and not exposed to consumers on their own compile classpath. implementation("com.google.flatbuffers:flatbuffers-java:22.10.26") implementation("commons-cli:commons-cli:1.8.0") - implementation("com.fasterxml.jackson.core:jackson-databind:2.15.1") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.13.5") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.5") implementation("com.github.jonpeterson:jackson-module-model-versioning:1.2.2") implementation("org.apache.commons:commons-math3:3.6.1") diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt index ac9f15d8d0..9cb045e014 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt @@ -13,6 +13,7 @@ import io.github.axisangles.ktmath.Quaternion.Companion.fromRotationVector import io.github.axisangles.ktmath.Vector3 import kotlinx.coroutines.* import solarxr_protocol.rpc.ResetType +import java.io.IOException import java.net.DatagramPacket import java.net.DatagramSocket import java.net.InetSocketAddress @@ -329,7 +330,12 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker bb.limit(bb.capacity()) bb.rewind() parser.write(bb, conn, UDPPacket1Heartbeat) - socket.send(DatagramPacket(rcvBuffer, bb.position(), conn.address)) + + try { + socket.send(DatagramPacket(rcvBuffer, bb.position(), conn.address)) + } catch (e: IOException) { + LogManager.warning("[TrackerServer] Failed to send package to $conn", e) + } if (conn.lastPacket + 1000 < System.currentTimeMillis()) { if (!conn.timedOut) { conn.timedOut = true @@ -367,7 +373,11 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker bb.putInt(10) bb.putLong(0) bb.putInt(conn.lastPingPacketId) - socket.send(DatagramPacket(rcvBuffer, bb.position(), conn.address)) + try { + socket.send(DatagramPacket(rcvBuffer, bb.position(), conn.address)) + } catch (e: IOException) { + LogManager.warning("[TrackerServer] Failed to send package to $conn", e) + } } } } diff --git a/server/ios/.gitignore b/server/ios/.gitignore new file mode 100644 index 0000000000..75cf992c40 --- /dev/null +++ b/server/ios/.gitignore @@ -0,0 +1,3 @@ +/build +robovm-build +/*local.properties diff --git a/server/ios/Info.plist.xml b/server/ios/Info.plist.xml new file mode 100644 index 0000000000..67cfac6cf0 --- /dev/null +++ b/server/ios/Info.plist.xml @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${app.name} + CFBundleExecutable + ${app.executable} + CFBundleIdentifier + ${app.id} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${app.name} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${app.version} + CFBundleSignature + ???? + CFBundleVersion + ${app.build} + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + Launch Screen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/server/ios/build.gradle.kts b/server/ios/build.gradle.kts new file mode 100644 index 0000000000..edef04d913 --- /dev/null +++ b/server/ios/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + `java-library` + id("robovm") + id("org.ajoberstar.grgit") +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +allprojects { + repositories { + mavenCentral() +// mavenLocal() + maven(url = "https://jitpack.io") + maven { + url = uri("https://central.sonatype.com/repository/maven-snapshots/") + } + } +} + +dependencies { + val robovmVersion = rootProject.properties["robovmVersion"] as String + + implementation(project(":server:core")) + implementation("com.robovmx:robovm-rt:$robovmVersion") + implementation("com.robovmx:robovm-cocoatouch:$robovmVersion") + implementation("com.fasterxml.jackson.core:jackson-databind:2.13.5") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.5") + implementation("org.slf4j:slf4j-simple:2.0.7") +} + +tasks.launchIPhoneSimulator { + dependsOn(tasks.build) +} +tasks.launchIPadSimulator { + dependsOn(tasks.build) +} +tasks.launchIOSDevice { + dependsOn(tasks.build) +} +tasks.robovmArchive { + dependsOn(tasks.build) +} + +tasks.register("makeLocalProperties") { + File("${project.projectDir}/robovm.local.properties").writeText( + """ + app.version=${grgit.describe(mapOf("tags" to true, "always" to true))} + app.build=${grgit.tag.list().size} + """.trimIndent(), + ) +} + +tasks.build { + dependsOn(":server:ios:makeLocalProperties") +} + +robovm { + isIosSkipSigning = true +} diff --git a/server/ios/resources/Base.lproj/Launch Screen.storyboard b/server/ios/resources/Base.lproj/Launch Screen.storyboard new file mode 100644 index 0000000000..737972c25e --- /dev/null +++ b/server/ios/resources/Base.lproj/Launch Screen.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png new file mode 100644 index 0000000000..d842573041 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000000..c5b356d2f8 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png new file mode 100644 index 0000000000..c5b356d2f8 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png new file mode 100644 index 0000000000..3d49086535 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png new file mode 100644 index 0000000000..258d802ca4 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000000..37ae061bad Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png new file mode 100644 index 0000000000..37ae061bad Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png new file mode 100644 index 0000000000..5d85316ce5 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png new file mode 100644 index 0000000000..c5b356d2f8 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000000..cfc94a0e7b Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png new file mode 100644 index 0000000000..cfc94a0e7b Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png new file mode 100644 index 0000000000..28bb14ec3c Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 0000000000..4471a53300 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-512@2x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png new file mode 100644 index 0000000000..28bb14ec3c Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png new file mode 100644 index 0000000000..6916323ca5 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png new file mode 100644 index 0000000000..19e5a1f2b8 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png new file mode 100644 index 0000000000..548f993305 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000..9a9640e238 Binary files /dev/null and b/server/ios/resources/Images.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png differ diff --git a/server/ios/resources/Images.xcassets/AppIcon.appiconset/Contents.json b/server/ios/resources/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9664c57335 --- /dev/null +++ b/server/ios/resources/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "AppIcon-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "AppIcon-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "AppIcon-20x20@2x 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-29x29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-40x40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "AppIcon-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "AppIcon-83.5x83.5@2x 1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "AppIcon-512@2x 1.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "AppIcon-20x20@1x.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "20x20" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/server/ios/resources/Images.xcassets/Contents.json b/server/ios/resources/Images.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/server/ios/resources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/server/ios/resources/dist b/server/ios/resources/dist new file mode 120000 index 0000000000..5c39568289 --- /dev/null +++ b/server/ios/resources/dist @@ -0,0 +1 @@ +../../../gui/dist \ No newline at end of file diff --git a/server/ios/robovm.properties b/server/ios/robovm.properties new file mode 100644 index 0000000000..4062671952 --- /dev/null +++ b/server/ios/robovm.properties @@ -0,0 +1,6 @@ +app.version=1.0 +app.id=dev.slimevr.ios +app.mainclass=dev.slimevr.ios.Main +app.executable=Main +app.build=1 +app.name=SlimeVR \ No newline at end of file diff --git a/server/ios/robovm.xml b/server/ios/robovm.xml new file mode 100644 index 0000000000..80b393a734 --- /dev/null +++ b/server/ios/robovm.xml @@ -0,0 +1,36 @@ + + ${app.executable} + ${app.mainclass} + ios + ios + Info.plist.xml + + + resources + + + + + + dev.slimevr.** + io.eiren.** + io.github.axisangles.** + com.jme3.** + java.util.logging.SimpleFormatter + java.util.logging.LoggingProxyImpl + android.icu.impl.TimeZoneNamesFactoryImpl + com.fasterxml.jackson.databind.** + com.google.flatbuffers.** + kotlin.reflect.jvm.internal.ReflectionFactoryImpl + + + WebKit + + + + + -ld_classic + + + + diff --git a/server/ios/src/main/java/dev/slimevr/ios/Main.java b/server/ios/src/main/java/dev/slimevr/ios/Main.java new file mode 100644 index 0000000000..197130ee57 --- /dev/null +++ b/server/ios/src/main/java/dev/slimevr/ios/Main.java @@ -0,0 +1,166 @@ +package dev.slimevr.ios; + +import dev.slimevr.ios.logging.FoundationLogPrintStream; +import org.robovm.apple.dispatch.DispatchQueue; +import org.robovm.apple.foundation.*; +import org.robovm.apple.uikit.UIApplication; +import org.robovm.apple.uikit.UIApplicationDelegateAdapter; +import org.robovm.apple.uikit.UIApplicationLaunchOptions; +import org.robovm.apple.uikit.UIWindow; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; + +import dev.slimevr.Keybinding; +import dev.slimevr.VRServer; +import io.eiren.util.logging.LogManager; +import org.robovm.rt.bro.ptr.BytePtr; + + +public class Main extends UIApplicationDelegateAdapter { + private UIWindow window; + private WebviewController rootViewController; + + @Override + public boolean didFinishLaunching( + UIApplication application, + UIApplicationLaunchOptions launchOptions + ) { + // Set up the view controller. + rootViewController = new WebviewController(); + + // Create a new window at screen size. + window = new UIWindow(); + // Set the view controller as the root controller for the window. + window.setRootViewController(rootViewController); + // Make the window visible. + window.makeKeyAndVisible(); + + return true; + } + + private static NSURL getAppFolder() { + try { + return NSFileManager + .getDefaultManager() + .getURLForDirectory( + NSSearchPathDirectory.DocumentDirectory, + NSSearchPathDomainMask.UserDomainMask, + null, + false + ); + } catch (NSErrorException e) { + throw new RuntimeException(e); + } + } + + private static String getString(NSURL url) { + var buffer = new BytePtr(); + boolean test = url.getFileSystemRepresentation(buffer, 1024L); + if (!test) + throw new RuntimeException("Couldn't fit URL into buffer"); + return buffer.toStringZ(); + } + + public static void main(String[] args) { + try (NSAutoreleasePool pool = new NSAutoreleasePool()) { + UIApplication.main(args, null, Main.class); + } + } + + @Override + public void didEnterBackground(UIApplication application) { + if (VRServer.Companion.getInstanceInitialized()) { + VRServer.Companion.getInstance().interrupt(); + try { + VRServer.Companion.getInstance().join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + super.didEnterBackground(application); + } + + @Override + public void willTerminate(UIApplication application) { + if (VRServer.Companion.getInstanceInitialized()) { + VRServer.Companion.getInstance().interrupt(); + try { + VRServer.Companion.getInstance().join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + super.willTerminate(application); + } + + public static void runServer() { + var thread = new Thread(() -> { + try { + LogManager.initialize(new File(getString(getAppFolder()))); + System.setErr(new FoundationLogPrintStream()); + System.setOut(new FoundationLogPrintStream()); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String sStackTrace = sw.toString(); + Foundation.log("%@\n%@", new NSString(e.toString()), new NSString(sStackTrace)); + } + try { + var vrServer = new VRServer( + getString( + getAppFolder() + .newURLByAppendingPathComponent("vrserver.yml") + ) + ); + vrServer.start(); + vrServer.addOnTick(new Runnable() { + int tick = 0; + + @Override + public void run() { + if (tick++ >= 1000) { + tick = 0; + final boolean hasTrackers = vrServer + .getAllTrackers() + .stream() + .anyMatch( + (tracker) -> !tracker.isComputed() + && tracker.getStatus().getSendData() + ); + DispatchQueue + .getMainQueue() + .sync( + () -> UIApplication + .getSharedApplication() + .setIdleTimerDisabled(hasTrackers) + ); + } + } + }); + new Keybinding(vrServer); + vrServer.join(); + LogManager.closeLogger(); + System.exit(0); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String sStackTrace = sw.toString(); + Foundation.log("%@\n%@", new NSString(e.toString()), new NSString(sStackTrace)); + } + }, "SlimeVR Main Thread"); + thread.setUncaughtExceptionHandler((th, e) -> { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String sStackTrace = sw.toString(); + Foundation.log("%@\n%@", new NSString(e.toString()), new NSString(sStackTrace)); + }); + thread.start(); + } +} diff --git a/server/ios/src/main/java/dev/slimevr/ios/WebviewController.java b/server/ios/src/main/java/dev/slimevr/ios/WebviewController.java new file mode 100644 index 0000000000..11cbed320d --- /dev/null +++ b/server/ios/src/main/java/dev/slimevr/ios/WebviewController.java @@ -0,0 +1,116 @@ +package dev.slimevr.ios; + +import dev.slimevr.VRServer; +import org.robovm.apple.foundation.*; +import org.robovm.apple.uikit.*; +import org.robovm.apple.uniformtypeid.UTType; +import org.robovm.apple.webkit.*; + +import java.util.List; + + +public class WebviewController extends UIViewController { + private WKWebView webView; + + public WebviewController() { + UIView view = getView(); + + view.setBackgroundColor(UIColor.purple()); + } + + @Override + public void loadView() { + super.loadView(); + UIView view = getView(); + + var config = new WKWebViewConfiguration(); + config.setURLSchemeHandler(new WKURLSchemeHandler() { + @Override + public void startURLSchemeTask(WKWebView webView, WKURLSchemeTask urlSchemeTask) { + var url = urlSchemeTask.getRequest().getURL(); + var fileUrl = fileUrlFromUrl(url); + if (fileUrl == null) + return; + var mimeType = mimeType(fileUrl); + var data = NSData.read(fileUrl); + if (data == null) + return; + + var response = new NSHTTPURLResponse(url, mimeType, data.getLength(), null); + + urlSchemeTask.didReceiveResponse(response); + urlSchemeTask.didReceiveData(data); + urlSchemeTask.didFinish(); + } + + @Override + public void stopURLSchemeTask(WKWebView webView, WKURLSchemeTask urlSchemeTask) { + + } + + private NSURL fileUrlFromUrl(NSURL url) { + List paths = url.getPathComponents(); + if (paths.size() == 1) { + return NSBundle.getMainBundle().findResourceURL("index.html", "", "dist"); + } + String last = paths.remove(paths.size() - 1); + paths.remove(0); // Remove "/" + StringBuilder joining = new StringBuilder(); + for (String path : paths) { + joining.append("/").append(path); + } + return NSBundle.getMainBundle().findResourceURL(last, "", "dist" + joining); + } + + private String mimeType(NSURL url) { + var type = UTType.createUsingFilenameExtension(url.getPathExtension()); + if (type == null) + return null; + return type.getPreferredMIMEType(); + } + }, "slimevr"); + var userContent = new WKUserContentController(); + userContent + .addUserScript( + new WKUserScript( + "window.__IOS__ = true;", + WKUserScriptInjectionTime.AtDocumentEnd, + false + ) + ); + userContent.addUserScript(makeZoomScale()); + config.setUserContentController(userContent); + webView = new WKWebView(view.getFrame(), config); + if (webView != null) { + view.addSubview(webView); + } + } + + @Override + public void viewDidLoad() { + super.viewDidLoad(); + + if (!VRServer.Companion.getInstanceInitialized()) { + Main.runServer(); + } + webView + .getScrollView() + .setContentInsetAdjustmentBehavior(UIScrollViewContentInsetAdjustmentBehavior.Never); + webView.getScrollView().setScrollEnabled(false); + var req = new NSURLRequest(new NSURL("slimevr:///")); + webView.loadRequest(req); + } + + private WKUserScript makeZoomScale() { + final String source = "var meta = document.createElement('meta');" + + + "meta.name = 'viewport';" + + + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" + + + "var head = document.getElementsByTagName('head')[0];" + + + "head.appendChild(meta);"; + return new WKUserScript(source, WKUserScriptInjectionTime.AtDocumentEnd, true); + } +} diff --git a/server/ios/src/main/java/dev/slimevr/ios/logging/FoundationLogPrintStream.java b/server/ios/src/main/java/dev/slimevr/ios/logging/FoundationLogPrintStream.java new file mode 100644 index 0000000000..812cb45529 --- /dev/null +++ b/server/ios/src/main/java/dev/slimevr/ios/logging/FoundationLogPrintStream.java @@ -0,0 +1,13 @@ +package dev.slimevr.ios.logging; + +import org.robovm.apple.foundation.Foundation; + + +public class FoundationLogPrintStream extends LoggingPrintStream { + + @Override + public void log(String text) { + Foundation.log(text); + } + +} diff --git a/server/ios/src/main/java/dev/slimevr/ios/logging/LoggingPrintStream.java b/server/ios/src/main/java/dev/slimevr/ios/logging/LoggingPrintStream.java new file mode 100644 index 0000000000..d12b2593a8 --- /dev/null +++ b/server/ios/src/main/java/dev/slimevr/ios/logging/LoggingPrintStream.java @@ -0,0 +1,142 @@ +package dev.slimevr.ios.logging; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + + +public abstract class LoggingPrintStream extends PrintStream { + StringBuffer st = new StringBuffer(); + + // + + public LoggingPrintStream() { + super(new OutputStream() { + @Override + public void write(int arg0) throws IOException { + } + }); + } + + public abstract void log(String text); + + public void write(char ch) { + if (ch == 0xa) {} else + st.append(ch); + } + + @Override + public void flush() { + if (st.length() > 0) { + log(st.toString()); + st.setLength(0); + } + } + + // + + @Override + public void print(char[] s) { + for (char ch : s) + write(ch); + } + + @Override + public void print(boolean b) { + print(b + ""); + } + + @Override + public void print(char c) { + write(c); + } + + @Override + public void print(double d) { + print(d + ""); + } + + @Override + public void print(float f) { + print(f + ""); + } + + @Override + public void print(int i) { + print(i + ""); + } + + public void print(long l) { + print(l + ""); + } + + @Override + public void print(Object obj) { + print((obj + "").toCharArray()); + } + + @Override + public void print(String s) { + print((s + "").toCharArray()); + } + + @Override + public void println() { + flush(); + } + + @Override + public void println(boolean x) { + print(x); + flush(); + } + + @Override + public void println(char x) { + print(x); + flush(); + } + + @Override + public void println(char[] x) { + print(x); + flush(); + } + + @Override + public void println(double x) { + print(x); + flush(); + } + + @Override + public void println(float x) { + print(x); + flush(); + } + + @Override + public void println(int x) { + print(x); + flush(); + } + + @Override + public void println(long x) { + print(x); + flush(); + } + + @Override + public void println(Object x) { + print(x); + flush(); + } + + @Override + public void println(String x) { + print(x); + flush(); + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e2691d0b82..1dafc0e222 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,11 +10,24 @@ rootProject.name = "SlimeVR Server" pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } + repositories { + gradlePluginPortal() + google() + mavenCentral() +// mavenLocal() + maven { + url = uri("https://central.sonatype.com/repository/maven-snapshots/") + } + } + + val robovmVersion: String by settings + resolutionStrategy { + eachPlugin { + if (requested.id.name == "robovm") { + useModule("com.robovmx:robovm-gradle-plugin:${requested.version ?: robovmVersion}") + } + } + } val kotlinVersion: String by settings val spotlessVersion: String by settings @@ -29,6 +42,7 @@ pluginManagement { id("com.gradleup.shadow") version shadowJarVersion id("com.github.gmazzo.buildconfig") version buildconfigVersion id("org.ajoberstar.grgit") version grgitVersion + id("robovm") version robovmVersion apply false } } @@ -40,3 +54,4 @@ project(":server").projectDir = File("server") include(":server:core") include(":server:desktop") include(":server:android") +include(":server:ios")