├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── deploy.yml │ ├── gradle.yml │ └── mkdocs.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── calf-camera-picker ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── camerapicker │ │ └── CameraPickerLauncher.android.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── camerapicker │ │ └── CameraPickerLauncher.kt │ ├── desktopMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── camerapicker │ │ └── CameraPickerLauncher.desktop.kt │ ├── iosMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── camerapicker │ │ └── CameraPickerLauncher.ios.kt │ └── webMain │ └── kotlin │ └── com │ └── mohamedrejeb │ └── calf │ └── camerapicker │ └── CameraPickerLauncher.web.kt ├── calf-core ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── core │ │ ├── LocalPlatformContext.android.kt │ │ └── PlatformContext.android.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── core │ │ ├── ExperimentalCalfApi.kt │ │ ├── InternalCalfApi.kt │ │ ├── LocalPlatformContext.kt │ │ └── PlatformContext.kt │ └── nonAndroidMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── core │ ├── LocalPlatformContext.nonAndroid.kt │ └── PlatformContext.nonAndroid.kt ├── calf-file-picker-coil ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── picker │ │ └── coil │ │ └── KmpFileFetcher.android.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── picker │ │ └── coil │ │ └── KmpFileFetcher.kt │ └── nonAndroidMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── picker │ └── coil │ └── KmpFileFetcher.nonAndroid.kt ├── calf-file-picker ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── picker │ │ ├── ComposeResource.android.kt │ │ └── FilePickerLauncher.android.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── picker │ │ ├── ComposeResource.kt │ │ └── FilePickerLauncher.kt │ ├── desktopMain │ ├── kotlin │ │ ├── com │ │ │ └── mohamedrejeb │ │ │ │ └── calf │ │ │ │ └── picker │ │ │ │ ├── ComposeResource.desktop.kt │ │ │ │ ├── FilePickerLauncher.desktop.kt │ │ │ │ ├── FileSaverLauncher.desktop.kt │ │ │ │ └── platform │ │ │ │ ├── PlatformFilePicker.kt │ │ │ │ ├── awt │ │ │ │ ├── AwtFilePicker.kt │ │ │ │ └── AwtFileSaver.kt │ │ │ │ ├── mac │ │ │ │ ├── MacOSFilePicker.kt │ │ │ │ └── foundation │ │ │ │ │ ├── Foundation.kt │ │ │ │ │ ├── FoundationLibrary.kt │ │ │ │ │ └── ID.kt │ │ │ │ ├── util │ │ │ │ └── Platform.kt │ │ │ │ └── windows │ │ │ │ ├── WindowsFilePicker.kt │ │ │ │ ├── api │ │ │ │ ├── JnaFileChooser.kt │ │ │ │ ├── WindowsFileChooser.kt │ │ │ │ └── WindowsFolderBrowser.kt │ │ │ │ └── win32 │ │ │ │ ├── Comdlg32.kt │ │ │ │ ├── Ole32.kt │ │ │ │ └── Shell32.kt │ │ └── jodd │ │ │ ├── io │ │ │ └── IOUtil.kt │ │ │ ├── net │ │ │ └── MimeTypes.kt │ │ │ └── util │ │ │ └── Wildcard.kt │ └── resources │ │ └── jodd │ │ └── net │ │ └── MimeTypes.properties │ ├── desktopTest │ └── kotlin │ │ └── jodd │ │ └── net │ │ └── MimeTypesTest.kt │ ├── iosMain │ └── kotlin │ │ └── com.mohamedrejeb.calf.picker │ │ ├── ComposeResource.ios.kt │ │ ├── FilePickerLauncher.ios.kt │ │ └── TempFile.kt │ ├── jsMain │ └── kotlin │ │ └── com.mohamedrejeb.calf.picker │ │ ├── ComposeResource.js.kt │ │ └── FilePickerLauncher.js.kt │ └── wasmJsMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── picker │ ├── ComposeResource.wasmJs.kt │ └── FilePickerLauncher.wasmJs.kt ├── calf-geo ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── geo │ │ └── LocationTracker.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── geo │ │ ├── Altitude.kt │ │ ├── Azimuth.kt │ │ ├── ExtendedLocation.kt │ │ ├── LatLng.kt │ │ ├── Location.kt │ │ ├── LocationTracker.kt │ │ └── Speed.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── geo │ │ └── LocationTracker.kt │ ├── iosMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── geo │ │ └── LocationTracker.kt │ ├── jsMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── geo │ │ └── LocationTracker.js.kt │ └── wasmJsMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── geo │ └── LocationTracker.wasmJs.kt ├── calf-io ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── io │ │ └── KmpFile.android.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── io │ │ └── KmpFile.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── io │ │ └── KmpFile.desktop.kt │ ├── iosMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── io │ │ └── KmpFile.ios.kt │ ├── jsMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── io │ │ └── KmpFile.js.kt │ └── wasmJsMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── io │ └── KmpFile.wasmJs.kt ├── calf-maps └── build.gradle.kts ├── calf-media └── build.gradle.kts ├── calf-navigation ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── navigation │ │ ├── AndroidBackHandler.android.kt │ │ └── NavHostController.android.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── navigation │ │ ├── AdaptiveBaseBundle.kt │ │ ├── AdaptiveBundle.kt │ │ ├── AdaptiveNavHost.kt │ │ ├── AdaptiveNavHostController.kt │ │ ├── AdaptiveNavType.kt │ │ ├── AndroidBackHandler.kt │ │ ├── NavArgument.kt │ │ ├── NavDestination.kt │ │ ├── NavGraphBuilder.kt │ │ └── NavHostController.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── navigation │ │ ├── AndroidBackHandler.desktop.kt │ │ └── NavHostController.desktop.kt │ ├── iosMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── navigation │ │ ├── AndroidBackHandler.ios.kt │ │ └── NavHostController.ios.kt │ ├── jsMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── navigation │ │ ├── AndroidBackHandler.js.kt │ │ └── NavHostController.ios.kt │ └── wasmJsMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── navigation │ ├── AndroidBackHandler.wasmJs.kt │ └── NavHostController.wasmJs.kt ├── calf-notifications ├── build.gradle.kts └── src │ └── iosMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── notifications │ └── Main.kt ├── calf-permissions ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── permissions │ │ ├── MutableMultiplePermissionsState.android.kt │ │ ├── MutablePermissionState.android.kt │ │ └── PermissionsUtil.android.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── permissions │ │ ├── MultiplePermissionsState.kt │ │ ├── MutableMultiplePermissionsState.kt │ │ ├── MutablePermissionState.kt │ │ ├── PermissionState.kt │ │ └── PermissionsUtil.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── permissions │ │ ├── MutableMultiplePermissionsState.desktop.kt │ │ └── MutablePermissionState.desktop.kt │ ├── iosMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── permissions │ │ ├── MutableMultiplePermissionsState.ios.kt │ │ ├── MutablePermissionState.ios.kt │ │ ├── PermissionsUtil.ios.kt │ │ └── helper │ │ ├── AVCapturePermissionHelper.kt │ │ ├── BluetoothPermissionHelper.kt │ │ ├── CalendarPermissionHelper.kt │ │ ├── ContactPermissionHelper.kt │ │ ├── GalleryPermissionHelper.kt │ │ ├── GrantedPermissionHelper.kt │ │ ├── LocalNotificationPermissionHelper.kt │ │ ├── LocationPermissionHelper.kt │ │ ├── PermissionHelper.kt │ │ └── RemoteNotificationPermissionHelper.kt │ ├── jsMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── permissions │ │ ├── MutableMultiplePermissionsState.js.kt │ │ └── MutablePermissionState.js.kt │ └── wasmJsMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── permissions │ ├── MutableMultiplePermissionsState.wasmJs.kt │ └── MutablePermissionState.wasmJs.kt ├── calf-sf-symbols ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── com.mohamedrejeb.calf.sf.symbols │ └── SFSymbols.kt ├── calf-ui ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── ui │ │ └── datepicker │ │ └── AdaptiveDatePickerState.android.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── ui │ │ ├── ExperimentalCalfUiApi.kt │ │ ├── cupertino │ │ └── CupertinoCircularProgressIndicator.kt │ │ ├── datepicker │ │ ├── AdaptiveDatePicker.kt │ │ ├── AdaptiveDatePickerState.kt │ │ └── UIKitDisplayMode.kt │ │ ├── dialog │ │ ├── AdaptiveAlertDialog.kt │ │ └── uikit │ │ │ ├── AlertDialogIosAction.kt │ │ │ ├── AlertDialogIosActionStyle.kt │ │ │ ├── AlertDialogIosProperties.kt │ │ │ ├── AlertDialogIosSeverity.kt │ │ │ ├── AlertDialogIosStyle.kt │ │ │ └── AlertDialogIosTextField.kt │ │ ├── dropdown │ │ └── AdaptiveDropDownMenu.kt │ │ ├── gesture │ │ └── AdaptiveClickable.kt │ │ ├── progress │ │ └── AdaptiveCircularProgressIndicator.kt │ │ ├── sheet │ │ ├── AdaptiveBottomSheet.kt │ │ └── AdaptiveSheetState.kt │ │ ├── timepicker │ │ ├── AdaptiveTimePicker.kt │ │ └── AdaptiveTimePickerState.kt │ │ ├── toggle │ │ ├── AdaptiveSwitch.kt │ │ └── CupertinoSwitch.kt │ │ └── uikit │ │ └── IosKeyboardType.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── ui │ │ └── datepicker │ │ └── AdaptiveDatePickerState.desktop.kt │ ├── iosMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── ui │ │ ├── datepicker │ │ ├── AdaptiveDatePicker.ios.kt │ │ ├── AdaptiveDatePickerState.ios.kt │ │ └── DatePickerManager.kt │ │ ├── dialog │ │ ├── AdaptiveAlertDialog.ios.kt │ │ └── AlertDialogManager.kt │ │ ├── dropdown │ │ └── AdaptiveDropDownMenu.ios.kt │ │ ├── gesture │ │ └── AdaptiveClickable.ios.kt │ │ ├── progress │ │ └── AdaptiveCircularProgressIndicator.ios.kt │ │ ├── sheet │ │ ├── AdaptiveBottomSheet.ios.kt │ │ ├── BottomSheetManager.kt │ │ └── SheetState.ios.kt │ │ ├── timepicker │ │ ├── AdaptiveTimePicker.ios.kt │ │ ├── AdaptiveTimePickerState.ios.kt │ │ └── TimePickerManager.kt │ │ ├── toggle │ │ └── AdaptiveSwitch.ios.kt │ │ └── utils │ │ ├── ColorHelper.kt │ │ ├── IosKeyboardTypeUtils.kt │ │ ├── ThemeUtils.kt │ │ └── datetime │ │ └── KotlinxDatetimeCalendarModel.kt │ ├── jsMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── ui │ │ └── datepicker │ │ └── AdaptiveDatePickerState.js.kt │ ├── materialMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── ui │ │ ├── datepicker │ │ ├── AdaptiveDatePicker.material.kt │ │ └── AdaptiveDatePickerState.material.kt │ │ ├── dialog │ │ └── AdaptiveAlertDialog.material.kt │ │ ├── dropdown │ │ └── AdaptiveDropDownMenu.material.kt │ │ ├── gesture │ │ └── AdaptiveClickable.material.kt │ │ ├── progress │ │ └── AdaptiveCircularProgressIndicator.material.kt │ │ ├── sheet │ │ ├── AdaptiveBottomSheet.material.kt │ │ └── AdaptiveSheetState.material.kt │ │ ├── timepicker │ │ ├── AdaptiveTimePicker.material.kt │ │ └── AdaptiveTimePickerState.material.kt │ │ └── toggle │ │ └── AdaptiveSwitch.material.kt │ └── wasmJsMain │ └── kotlin │ └── com │ └── mohamedrejeb │ └── calf │ └── ui │ └── datepicker │ └── AdaptiveDatePickerState.wasmJs.kt ├── calf-webview ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── mohamedrejeb │ │ └── calf │ │ └── ui │ │ └── web │ │ └── WebView.android.kt │ ├── commonMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── ui │ │ └── web │ │ ├── WebSettings.kt │ │ └── WebView.kt │ ├── desktopMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── ui │ │ └── web │ │ └── WebView.desktop.kt │ ├── iosMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── ui │ │ └── web │ │ └── WebView.ios.kt │ ├── jsMain │ └── kotlin │ │ └── com.mohamedrejeb.calf │ │ └── ui │ │ └── web │ │ └── WebView.js.kt │ └── wasmJsMain │ └── kotlin │ └── com.mohamedrejeb.calf │ └── ui │ └── web │ └── WebView.wasmJs.kt ├── cleanup.sh ├── convention-plugins ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── Android.kt │ ├── GradleSetupPlugin.kt │ ├── Hirearchy.kt │ ├── Targets.kt │ ├── android.library.gradle.kts │ ├── compose.multiplatform.gradle.kts │ ├── kotlin.multiplatform.gradle.kts │ ├── module.publication.gradle.kts │ └── root.publication.gradle.kts ├── docs ├── code_of_conduct.md ├── filepicker.md ├── images │ ├── AdaptiveAlertDialog-android.png │ ├── AdaptiveAlertDialog-ios-action-sheet.png │ ├── AdaptiveAlertDialog-ios-with-text-field.png │ ├── AdaptiveAlertDialog-ios.png │ ├── AdaptiveBottomSheet-android.png │ ├── AdaptiveBottomSheet-ios.png │ ├── AdaptiveCircularProgressIndicator-android.png │ ├── AdaptiveCircularProgressIndicator-ios.png │ ├── AdaptiveDatePicker-android.png │ ├── AdaptiveDatePicker-ios.png │ ├── AdaptiveFilePicker-android.png │ ├── AdaptiveFilePicker-desktop.png │ ├── AdaptiveFilePicker-ios.png │ ├── AdaptiveFilePicker-web.png │ ├── AdaptiveTimePicker-android.png │ ├── AdaptiveTimePicker-ios.png │ ├── Permissions-android.png │ ├── Permissions-ios.png │ ├── WebView-android.png │ ├── WebView-ios.png │ ├── logo.ico │ ├── logo.png │ ├── logo.svg │ └── thumbnail.png ├── index.md ├── installation.md ├── permissions.md ├── ui.md ├── ui │ ├── adaptive-alert-dialog.md │ ├── adaptive-bottom-sheet.md │ ├── adaptive-circular-progress-indicator.md │ ├── adaptive-clickable.md │ ├── adaptive-date-picker.md │ └── adaptive-time-picker.md └── webview.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store ├── wasm │ └── yarn.lock └── yarn.lock ├── mkdocs.yml ├── sample ├── android │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── mohamedrejeb │ │ │ └── calf │ │ │ └── sample │ │ │ ├── MainActivity.kt │ │ │ └── PermissionScreenPreview.kt │ │ └── res │ │ └── xml │ │ └── filepaths.xml ├── common │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com.mohamedrejeb.calf.sample │ │ │ └── Platform.android.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── com.mohamedrejeb.calf.sample │ │ │ ├── App.kt │ │ │ ├── Platform.kt │ │ │ ├── coil │ │ │ └── ImageLoader.kt │ │ │ ├── navigation │ │ │ ├── AppNavGraph.kt │ │ │ └── Screen.kt │ │ │ ├── screens │ │ │ ├── AdaptiveClickableScreen.kt │ │ │ ├── AlertDialogScreen.kt │ │ │ ├── BottomSheetScreen.kt │ │ │ ├── CameraPickerScreen.kt │ │ │ ├── DatePickerScreen.kt │ │ │ ├── DropDownMenuScreen.kt │ │ │ ├── FilePickerScreen.kt │ │ │ ├── HomeScreen.kt │ │ │ ├── ImagePickerScreen.kt │ │ │ ├── MapScreen.kt │ │ │ ├── PermissionsScreen.kt │ │ │ ├── ProgressBarScreen.kt │ │ │ ├── SwitchScreen.kt │ │ │ ├── TimePickerScreen.kt │ │ │ └── WebViewScreen.kt │ │ │ └── ui.theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Typography.kt │ │ ├── desktopMain │ │ └── kotlin │ │ │ └── com.mohamedrejeb.calf.sample │ │ │ └── Platform.desktop.kt │ │ ├── iosMain │ │ └── kotlin │ │ │ ├── Main.ios.kt │ │ │ ├── Second.ios.kt │ │ │ └── com.mohamedrejeb.calf.sample │ │ │ └── Platform.ios.kt │ │ ├── jsMain │ │ └── kotlin │ │ │ └── com.mohamedrejeb.calf.sample │ │ │ └── Platform.js.kt │ │ └── wasmJsMain │ │ └── kotlin │ │ └── com.mohamedrejeb.calf.sample │ │ └── Platform.wasmJs.kt ├── desktop │ ├── build.gradle.kts │ └── src │ │ └── jvmMain │ │ └── kotlin │ │ └── Main.kt ├── ios │ ├── Calf.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Calf │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── CalfApp.swift │ │ ├── ContentView.swift │ │ ├── Info.plist │ │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── web-js │ ├── build.gradle.kts │ ├── src │ │ └── jsMain │ │ │ ├── kotlin │ │ │ └── Main.kt │ │ │ └── resources │ │ │ └── index.html │ └── webpack.config.d │ │ └── fs.js └── web-wasm │ ├── build.gradle.kts │ └── src │ └── wasmJsMain │ ├── kotlin │ └── Main.kt │ └── resources │ └── index.html └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ktlint_code_style = ktlint_official 3 | 4 | ktlint_standard_multiline-if-else = disabled 5 | ktlint_standard_property-naming = disabled 6 | ktlint_standard_function_naming_ignore_when_annotated_with = Composable 7 | ktlint_function_naming_ignore_when_annotated_with = Composable -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://www.buymeacoffee.com/mohamedrejeb'] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 7 | 8 | name: Deploy to central 9 | 10 | on: workflow_dispatch 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | uses: ./.github/workflows/gradle.yml 18 | deploy: 19 | needs: build 20 | runs-on: macos-14 21 | 22 | env: 23 | OSSRH_STAGING_PROFILE_ID: ${{ secrets.OSSRH_STAGING_PROFILE_ID }} 24 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 25 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 26 | OSSRH_GPG_SECRET_KEY_PASSWORD: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 27 | OSSRH_GPG_SECRET_KEY_ID: ${{ secrets.OSSRH_GPG_SECRET_KEY_ID }} 28 | OSSRH_GPG_SECRET_KEY: ${{ secrets.OSSRH_GPG_SECRET_KEY }} 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Validate Gradle Wrapper 34 | uses: gradle/wrapper-validation-action@v3 35 | 36 | - uses: actions/cache@v4 37 | with: 38 | path: | 39 | ~/.konan 40 | key: ${{ runner.os }}-${{ hashFiles('**/.lock') }} 41 | 42 | - name: Set up JDK 17 43 | uses: actions/setup-java@v4 44 | with: 45 | java-version: '17' 46 | distribution: 'temurin' 47 | 48 | - name: Setup gradle 49 | uses: gradle/actions/setup-gradle@v4 50 | 51 | - name: Gradle publish 52 | run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 7 | 8 | name: Java CI with Gradle 9 | 10 | on: 11 | push: 12 | branches: [ "main" ] 13 | pull_request: 14 | branches: [ "main" ] 15 | workflow_call: 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | build: 22 | runs-on: macos-14 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Validate Gradle Wrapper 28 | uses: gradle/wrapper-validation-action@v3 29 | 30 | - uses: actions/cache@v4 31 | with: 32 | path: | 33 | ~/.konan 34 | key: ${{ runner.os }}-${{ hashFiles('**/.lock') }} 35 | 36 | - name: Set up JDK 17 37 | uses: actions/setup-java@v4 38 | with: 39 | java-version: '17' 40 | distribution: 'temurin' 41 | 42 | - name: Setup gradle 43 | uses: gradle/actions/setup-gradle@v4 44 | 45 | - name: Gradle test 46 | run: ./gradlew allTests 47 | 48 | deploy: 49 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 50 | needs: build 51 | runs-on: macos-14 52 | steps: 53 | - uses: actions/checkout@v4 54 | - name: Set up JDK 17 55 | uses: actions/setup-java@v4 56 | with: 57 | java-version: '17' 58 | distribution: 'temurin' 59 | 60 | - name: Setup Gradle 61 | uses: gradle/actions/setup-gradle@v4 62 | 63 | - name: Deploy snapshot 64 | env: 65 | VERSION: 0.8.0-SNAPSHOT 66 | OSSRH_STAGING_PROFILE_ID: ${{ secrets.OSSRH_STAGING_PROFILE_ID }} 67 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 68 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 69 | OSSRH_GPG_SECRET_KEY_PASSWORD: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 70 | OSSRH_GPG_SECRET_KEY_ID: ${{ secrets.OSSRH_GPG_SECRET_KEY_ID }} 71 | OSSRH_GPG_SECRET_KEY: ${{ secrets.OSSRH_GPG_SECRET_KEY }} 72 | run: ./gradlew publish -------------------------------------------------------------------------------- /.github/workflows/mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: mkdocs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | contents: write 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-python@v5 14 | with: 15 | python-version: 3.x 16 | - uses: actions/cache@v4 17 | with: 18 | key: ${{ github.ref }} 19 | path: .cache 20 | - run: pip install mkdocs-material 21 | - run: pip install pillow cairosvg 22 | - run: pip install mkdocs-minify-plugin 23 | - run: mkdocs gh-deploy --force -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | captures 4 | .externalNativeBuild 5 | .cxx 6 | xcuserdata 7 | .kotlin 8 | 9 | .gradle 10 | build/ 11 | !gradle/wrapper/gradle-wrapper.jar 12 | !**/src/main/**/build/ 13 | !**/src/test/**/build/ 14 | 15 | ### IntelliJ IDEA ### 16 | .idea/modules.xml 17 | .idea/jarRepositories.xml 18 | .idea/compiler.xml 19 | .idea/libraries/ 20 | *.iws 21 | *.iml 22 | *.ipr 23 | out/ 24 | !**/src/main/**/out/ 25 | !**/src/test/**/out/ 26 | 27 | ### Eclipse ### 28 | .apt_generated 29 | .classpath 30 | .factorypath 31 | .project 32 | .settings 33 | .springBeans 34 | .sts4-cache 35 | bin/ 36 | !**/src/main/**/bin/ 37 | !**/src/test/**/bin/ 38 | 39 | ### NetBeans ### 40 | /nbproject/private/ 41 | /nbbuild/ 42 | /dist/ 43 | /nbdist/ 44 | /.nb-gradle/ 45 | 46 | ### VS Code ### 47 | .vscode/ 48 | 49 | ### Mac OS ### 50 | .DS_Store 51 | 52 | ### Android ### 53 | local.properties 54 | 55 | ### Docs ### 56 | docs/api 57 | site 58 | 59 | ### Python ### 60 | venv/ 61 | 62 | .calf 63 | .calf/* 64 | .junie -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("root.publication") 3 | // trick: for the same plugin versions in all sub-modules 4 | alias(libs.plugins.androidApplication).apply(false) 5 | alias(libs.plugins.androidLibrary).apply(false) 6 | alias(libs.plugins.kotlinMultiplatform).apply(false) 7 | alias(libs.plugins.kotlinAndroid).apply(false) 8 | alias(libs.plugins.kotlinJvm).apply(false) 9 | alias(libs.plugins.kotlinSerialization).apply(false) 10 | alias(libs.plugins.composeCompiler).apply(false) 11 | alias(libs.plugins.composeMultiplatform).apply(false) 12 | } 13 | -------------------------------------------------------------------------------- /calf-camera-picker/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | id("module.publication") 4 | } 5 | 6 | kotlin { 7 | sourceSets.commonMain.dependencies { 8 | api(projects.calfIo) 9 | 10 | implementation(compose.runtime) 11 | implementation(compose.foundation) 12 | } 13 | 14 | sourceSets.commonTest.dependencies { 15 | implementation(libs.kotlin.test) 16 | } 17 | 18 | sourceSets.androidMain.dependencies { 19 | implementation(libs.activity.compose) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /calf-camera-picker/src/androidMain/kotlin/com/mohamedrejeb/calf/camerapicker/CameraPickerLauncher.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.camerapicker 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.net.Uri 6 | import android.os.Build 7 | import androidx.activity.compose.rememberLauncherForActivityResult 8 | import androidx.activity.result.ActivityResultLauncher 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.Stable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.core.content.FileProvider 18 | import com.mohamedrejeb.calf.io.KmpFile 19 | import java.io.File 20 | 21 | @Stable 22 | internal class CameraPickerLauncherAndroidImpl( 23 | private val context: Context, 24 | private val launcher: ActivityResultLauncher, 25 | private val onResult: (KmpFile) -> Unit 26 | ) : CameraPickerLauncher { 27 | 28 | private var imageUri by mutableStateOf(null) 29 | 30 | override fun launch() { 31 | if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) { 32 | return // No camera on this device 33 | } 34 | 35 | val file = File(context.cacheDir, "photo_${System.currentTimeMillis()}.jpg") 36 | val uri = if (Build.VERSION.SDK_INT >= 24) { 37 | FileProvider.getUriForFile(context, "${context.packageName}.provider", file) 38 | } else { 39 | Uri.fromFile(file) 40 | } 41 | 42 | imageUri = uri 43 | launcher.launch(uri) 44 | } 45 | 46 | fun handleResult(success: Boolean) { 47 | if (success && imageUri != null) { 48 | onResult(KmpFile(imageUri!!)) 49 | } 50 | } 51 | } 52 | 53 | @Composable 54 | actual fun rememberCameraPickerLauncher( 55 | onResult: (KmpFile) -> Unit, 56 | ): CameraPickerLauncher { 57 | val context = LocalContext.current 58 | 59 | val cameraPickerLauncher = remember { 60 | mutableStateOf(null) 61 | } 62 | 63 | val launcher = rememberLauncherForActivityResult( 64 | contract = ActivityResultContracts.TakePicture() 65 | ) { success -> 66 | cameraPickerLauncher.value?.handleResult(success) 67 | } 68 | 69 | return remember { 70 | CameraPickerLauncherAndroidImpl(context, launcher, onResult).also { 71 | cameraPickerLauncher.value = it 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /calf-camera-picker/src/commonMain/kotlin/com.mohamedrejeb.calf/camerapicker/CameraPickerLauncher.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.camerapicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.mohamedrejeb.calf.io.KmpFile 5 | 6 | 7 | interface CameraPickerLauncher { 8 | fun launch() 9 | } 10 | 11 | @Composable 12 | expect fun rememberCameraPickerLauncher( 13 | onResult: (KmpFile) -> Unit, 14 | ): CameraPickerLauncher 15 | 16 | 17 | -------------------------------------------------------------------------------- /calf-camera-picker/src/desktopMain/kotlin/com.mohamedrejeb.calf/camerapicker/CameraPickerLauncher.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.camerapicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.remember 6 | import com.mohamedrejeb.calf.io.KmpFile 7 | 8 | @Stable 9 | internal class CameraPickerLauncherDesktopImpl() : CameraPickerLauncher { 10 | override fun launch() = Unit 11 | } 12 | 13 | @Composable 14 | actual fun rememberCameraPickerLauncher( 15 | onResult: (KmpFile) -> Unit, 16 | ): CameraPickerLauncher { 17 | return remember { 18 | CameraPickerLauncherDesktopImpl() 19 | } 20 | } -------------------------------------------------------------------------------- /calf-camera-picker/src/webMain/kotlin/com/mohamedrejeb/calf/camerapicker/CameraPickerLauncher.web.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.camerapicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.remember 6 | import com.mohamedrejeb.calf.io.KmpFile 7 | 8 | @Stable 9 | internal class CameraPickerLauncherWebImpl() : CameraPickerLauncher { 10 | override fun launch() = Unit 11 | } 12 | 13 | @Composable 14 | actual fun rememberCameraPickerLauncher( 15 | onResult: (KmpFile) -> Unit, 16 | ): CameraPickerLauncher { 17 | return remember { 18 | CameraPickerLauncherWebImpl() 19 | } 20 | } -------------------------------------------------------------------------------- /calf-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | id("module.publication") 4 | } 5 | 6 | kotlin { 7 | sourceSets.commonMain.dependencies { 8 | implementation(compose.runtime) 9 | implementation(compose.foundation) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /calf-core/src/androidMain/kotlin/com.mohamedrejeb.calf/core/LocalPlatformContext.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.core 2 | 3 | import androidx.compose.ui.platform.LocalContext 4 | 5 | actual val LocalPlatformContext get() = LocalContext 6 | -------------------------------------------------------------------------------- /calf-core/src/androidMain/kotlin/com.mohamedrejeb.calf/core/PlatformContext.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.core 2 | 3 | import android.content.Context 4 | 5 | actual typealias PlatformContext = Context 6 | -------------------------------------------------------------------------------- /calf-core/src/commonMain/kotlin/com.mohamedrejeb.calf/core/ExperimentalCalfApi.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.core 2 | 3 | @RequiresOptIn( 4 | level = RequiresOptIn.Level.ERROR, 5 | message = "This is an experimental API for Calf and is likely to change before becoming " + 6 | "stable." 7 | ) 8 | @Target( 9 | AnnotationTarget.CLASS, 10 | AnnotationTarget.FUNCTION, 11 | AnnotationTarget.PROPERTY, 12 | AnnotationTarget.PROPERTY_GETTER 13 | ) 14 | @Retention(AnnotationRetention.BINARY) 15 | annotation class ExperimentalCalfApi 16 | -------------------------------------------------------------------------------- /calf-core/src/commonMain/kotlin/com.mohamedrejeb.calf/core/InternalCalfApi.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.core 2 | 3 | @RequiresOptIn( 4 | level = RequiresOptIn.Level.ERROR, 5 | message = "This is internal API for Calf modules that may change frequently " + 6 | "and without warning." 7 | ) 8 | @Target( 9 | AnnotationTarget.CLASS, 10 | AnnotationTarget.FUNCTION, 11 | AnnotationTarget.PROPERTY, 12 | AnnotationTarget.CONSTRUCTOR 13 | ) 14 | @Retention(AnnotationRetention.BINARY) 15 | annotation class InternalCalfApi 16 | -------------------------------------------------------------------------------- /calf-core/src/commonMain/kotlin/com.mohamedrejeb.calf/core/LocalPlatformContext.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.core 2 | 3 | import androidx.compose.runtime.ProvidableCompositionLocal 4 | 5 | expect val LocalPlatformContext: ProvidableCompositionLocal 6 | -------------------------------------------------------------------------------- /calf-core/src/commonMain/kotlin/com.mohamedrejeb.calf/core/PlatformContext.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.core 2 | 3 | expect abstract class PlatformContext 4 | -------------------------------------------------------------------------------- /calf-core/src/nonAndroidMain/kotlin/com.mohamedrejeb.calf/core/LocalPlatformContext.nonAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.core 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | 5 | actual val LocalPlatformContext = 6 | staticCompositionLocalOf { 7 | PlatformContext.INSTANCE 8 | } 9 | -------------------------------------------------------------------------------- /calf-core/src/nonAndroidMain/kotlin/com.mohamedrejeb.calf/core/PlatformContext.nonAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.core 2 | 3 | import kotlin.jvm.JvmField 4 | 5 | actual abstract class PlatformContext private constructor() { 6 | companion object { 7 | @JvmField 8 | val INSTANCE = object : PlatformContext() {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /calf-file-picker-coil/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kotlin.multiplatform") 3 | id("module.publication") 4 | } 5 | 6 | kotlin { 7 | sourceSets.commonMain.dependencies { 8 | api(projects.calfCore) 9 | api(projects.calfIo) 10 | implementation(libs.coil) 11 | } 12 | } -------------------------------------------------------------------------------- /calf-file-picker-coil/src/androidMain/kotlin/com.mohamedrejeb.calf/picker/coil/KmpFileFetcher.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.coil 2 | 3 | import com.mohamedrejeb.calf.core.PlatformContext 4 | 5 | actual fun coil3.PlatformContext.toCalfPlatformContext(): PlatformContext = 6 | this -------------------------------------------------------------------------------- /calf-file-picker-coil/src/commonMain/kotlin/com.mohamedrejeb.calf/picker/coil/KmpFileFetcher.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.coil 2 | 3 | import coil3.ImageLoader 4 | import coil3.decode.DataSource 5 | import coil3.decode.ImageSource 6 | import coil3.fetch.FetchResult 7 | import coil3.fetch.Fetcher 8 | import coil3.fetch.SourceFetchResult 9 | import coil3.request.Options 10 | import com.mohamedrejeb.calf.io.KmpFile 11 | import com.mohamedrejeb.calf.io.readByteArray 12 | import com.mohamedrejeb.calf.core.PlatformContext 13 | import okio.Buffer 14 | 15 | expect fun coil3.PlatformContext.toCalfPlatformContext(): PlatformContext 16 | 17 | class KmpFileFetcher( 18 | private val file: KmpFile, 19 | private val options: Options, 20 | ) : Fetcher { 21 | 22 | override suspend fun fetch(): FetchResult { 23 | return SourceFetchResult( 24 | source = ImageSource( 25 | source = Buffer().apply { 26 | write(file.readByteArray(options.context.toCalfPlatformContext())) 27 | }, 28 | fileSystem = options.fileSystem, 29 | ), 30 | mimeType = null, 31 | dataSource = DataSource.MEMORY, 32 | ) 33 | } 34 | 35 | class Factory : Fetcher.Factory { 36 | override fun create( 37 | data: KmpFile, 38 | options: Options, 39 | imageLoader: ImageLoader, 40 | ): Fetcher { 41 | return KmpFileFetcher(data, options) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /calf-file-picker-coil/src/nonAndroidMain/kotlin/com.mohamedrejeb.calf/picker/coil/KmpFileFetcher.nonAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.coil 2 | 3 | import com.mohamedrejeb.calf.core.PlatformContext 4 | 5 | actual fun coil3.PlatformContext.toCalfPlatformContext(): PlatformContext = 6 | PlatformContext.INSTANCE -------------------------------------------------------------------------------- /calf-file-picker/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | id("module.publication") 4 | } 5 | 6 | kotlin { 7 | sourceSets.commonMain.dependencies { 8 | api(projects.calfIo) 9 | 10 | implementation(compose.runtime) 11 | implementation(compose.foundation) 12 | } 13 | 14 | sourceSets.commonTest.dependencies { 15 | implementation(libs.kotlin.test) 16 | } 17 | 18 | sourceSets.androidMain.dependencies { 19 | implementation(libs.activity.compose) 20 | } 21 | 22 | sourceSets.desktopMain.dependencies { 23 | implementation(libs.jna) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /calf-file-picker/src/androidMain/kotlin/com/mohamedrejeb/calf/picker/ComposeResource.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import androidx.compose.ui.graphics.ImageBitmap 6 | import androidx.compose.ui.graphics.asImageBitmap 7 | 8 | actual fun ByteArray.toImageBitmap(): ImageBitmap = toAndroidBitmap().asImageBitmap() 9 | 10 | fun ByteArray.toAndroidBitmap(): Bitmap { 11 | return BitmapFactory.decodeByteArray(this, 0, size) 12 | } -------------------------------------------------------------------------------- /calf-file-picker/src/commonMain/kotlin/com.mohamedrejeb.calf/picker/ComposeResource.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | 5 | expect fun ByteArray.toImageBitmap(): ImageBitmap -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/ComposeResource.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import androidx.compose.ui.graphics.toComposeImageBitmap 5 | import org.jetbrains.skia.Image 6 | 7 | actual fun ByteArray.toImageBitmap(): ImageBitmap = 8 | Image.makeFromEncoded(this).toComposeImageBitmap() -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/FilePickerLauncher.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import com.mohamedrejeb.calf.io.KmpFile 7 | import com.mohamedrejeb.calf.picker.platform.PlatformFilePicker 8 | import kotlinx.coroutines.launch 9 | import java.io.File 10 | 11 | @Composable 12 | actual fun rememberFilePickerLauncher( 13 | type: FilePickerFileType, 14 | selectionMode: FilePickerSelectionMode, 15 | onResult: (List) -> Unit, 16 | ): FilePickerLauncher { 17 | val scope = rememberCoroutineScope() 18 | 19 | return remember { 20 | FilePickerLauncher( 21 | type = type, 22 | selectionMode = selectionMode, 23 | onLaunch = { 24 | scope.launch { 25 | if (type == FilePickerFileType.Folder) 26 | PlatformFilePicker.current.launchDirectoryPicker( 27 | initialDirectory = null, 28 | title = "Select a folder", 29 | parentWindow = null, 30 | onResult = { file -> 31 | onResult( 32 | if (file == null) 33 | emptyList() 34 | else 35 | listOf(KmpFile(file)) 36 | ) 37 | } 38 | ) 39 | else 40 | PlatformFilePicker.current.launchFilePicker( 41 | initialDirectory = null, 42 | type = type, 43 | selectionMode = selectionMode, 44 | title = "Select a file", 45 | parentWindow = null, 46 | onResult = { files -> 47 | onResult(files.map { KmpFile(it) }) 48 | } 49 | ) 50 | } 51 | }, 52 | ) 53 | } 54 | } 55 | 56 | actual class FilePickerLauncher actual constructor( 57 | type: FilePickerFileType, 58 | selectionMode: FilePickerSelectionMode, 59 | private val onLaunch: () -> Unit, 60 | ) { 61 | actual fun launch() { 62 | onLaunch() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/FileSaverLauncher.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import androidx.compose.runtime.rememberUpdatedState 7 | import com.mohamedrejeb.calf.core.ExperimentalCalfApi 8 | import com.mohamedrejeb.calf.io.KmpFile 9 | import com.mohamedrejeb.calf.picker.platform.awt.AwtFileSaver 10 | import kotlinx.coroutines.launch 11 | 12 | @ExperimentalCalfApi 13 | @Composable 14 | fun rememberFileSaverLauncher( 15 | onResult: (KmpFile?) -> Unit, 16 | ): FileSaverLauncher { 17 | val scope = rememberCoroutineScope() 18 | 19 | val currentOnResult by rememberUpdatedState(onResult) 20 | 21 | return FileSaverLauncher( 22 | onLaunch = { bytes, baseName, extension, initialDirectory -> 23 | scope.launch { 24 | val file = AwtFileSaver.saveFile( 25 | bytes = bytes, 26 | baseName = baseName, 27 | extension = extension, 28 | initialDirectory = initialDirectory, 29 | parentWindow = null, 30 | ) 31 | 32 | currentOnResult(file) 33 | } 34 | } 35 | ) 36 | } 37 | 38 | @ExperimentalCalfApi 39 | class FileSaverLauncher( 40 | private val onLaunch: ( 41 | bytes: ByteArray?, 42 | baseName: String, 43 | extension: String, 44 | initialDirectory: String?, 45 | ) -> Unit, 46 | ) { 47 | fun launch( 48 | bytes: ByteArray?, 49 | baseName: String, 50 | extension: String, 51 | initialDirectory: String?, 52 | ) { 53 | onLaunch( 54 | bytes, 55 | baseName, 56 | extension, 57 | initialDirectory, 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/platform/PlatformFilePicker.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.platform 2 | 3 | import com.mohamedrejeb.calf.picker.FilePickerFileType 4 | import com.mohamedrejeb.calf.picker.FilePickerSelectionMode 5 | import com.mohamedrejeb.calf.picker.platform.awt.AwtFilePicker 6 | import com.mohamedrejeb.calf.picker.platform.mac.MacOSFilePicker 7 | import com.mohamedrejeb.calf.picker.platform.util.Platform 8 | import com.mohamedrejeb.calf.picker.platform.util.PlatformUtil 9 | import com.mohamedrejeb.calf.picker.platform.windows.WindowsFilePicker 10 | import java.awt.Window 11 | import java.io.File 12 | 13 | interface PlatformFilePicker { 14 | 15 | suspend fun launchFilePicker( 16 | initialDirectory: String?, 17 | type: FilePickerFileType, 18 | selectionMode: FilePickerSelectionMode, 19 | title: String?, 20 | parentWindow: Window?, 21 | onResult: (List) -> Unit, 22 | ) 23 | 24 | suspend fun launchDirectoryPicker( 25 | initialDirectory: String?, 26 | title: String?, 27 | parentWindow: Window?, 28 | onResult: (File?) -> Unit, 29 | ) 30 | 31 | companion object { 32 | val current: PlatformFilePicker by lazy { createPlatformFilePicker() } 33 | 34 | private fun createPlatformFilePicker(): PlatformFilePicker { 35 | return when (PlatformUtil.current) { 36 | Platform.Windows -> 37 | WindowsFilePicker() 38 | 39 | Platform.MacOS -> 40 | MacOSFilePicker() 41 | 42 | Platform.Linux -> 43 | AwtFilePicker() 44 | } 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/platform/awt/AwtFileSaver.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.platform.awt 2 | 3 | import com.mohamedrejeb.calf.io.KmpFile 4 | import kotlinx.coroutines.suspendCancellableCoroutine 5 | import java.awt.Dialog 6 | import java.awt.FileDialog 7 | import java.awt.Frame 8 | import java.awt.Window 9 | import java.io.File 10 | import kotlin.coroutines.resume 11 | 12 | internal object AwtFileSaver { 13 | suspend fun saveFile( 14 | bytes: ByteArray?, 15 | baseName: String, 16 | extension: String, 17 | initialDirectory: String?, 18 | parentWindow: Window?, 19 | ): KmpFile? = suspendCancellableCoroutine { continuation -> 20 | fun handleResult(value: Boolean, files: Array?) { 21 | if (value) { 22 | val file = files?.firstOrNull()?.let { file -> 23 | // Write bytes to file, or create a new file 24 | bytes?.let { file.writeBytes(bytes) } ?: file.createNewFile() 25 | KmpFile(file) 26 | } 27 | continuation.resume(file) 28 | } 29 | } 30 | 31 | // Handle parentWindow: Dialog, Frame, or null 32 | val dialog = when (parentWindow) { 33 | is Dialog -> object : FileDialog(parentWindow, "Save dialog", SAVE) { 34 | override fun setVisible(value: Boolean) { 35 | super.setVisible(value) 36 | handleResult(value, files) 37 | } 38 | } 39 | 40 | else -> object : FileDialog(parentWindow as? Frame, "Save dialog", SAVE) { 41 | override fun setVisible(value: Boolean) { 42 | super.setVisible(value) 43 | handleResult(value, files) 44 | } 45 | } 46 | } 47 | 48 | // Set initial directory 49 | dialog.directory = initialDirectory 50 | 51 | // Set file name 52 | dialog.file = "$baseName.$extension" 53 | 54 | // Show the dialog 55 | dialog.isVisible = true 56 | 57 | // Dispose the dialog when the continuation is cancelled 58 | continuation.invokeOnCancellation { dialog.dispose() } 59 | } 60 | } -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/platform/mac/foundation/ID.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.platform.mac.foundation 2 | 3 | import com.sun.jna.NativeLong 4 | 5 | /** 6 | * Could be an address in memory (if pointer to a class or method) or a value (like 0 or 1) 7 | */ 8 | internal class ID : NativeLong { 9 | constructor() 10 | 11 | constructor(peer: Long) : super(peer) 12 | 13 | fun booleanValue(): Boolean { 14 | return toInt() != 0 15 | } 16 | 17 | override fun toByte(): Byte { 18 | return toLong().toByte() 19 | } 20 | 21 | override fun toShort(): Short { 22 | return toLong().toShort() 23 | } 24 | 25 | companion object { 26 | val NIL: ID = ID(0L) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/platform/util/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.platform.util 2 | 3 | internal object PlatformUtil { 4 | val current: Platform 5 | get() { 6 | val system = System.getProperty("os.name").lowercase() 7 | return when { 8 | system.contains("win") -> 9 | Platform.Windows 10 | 11 | system.contains("nix") || system.contains("nux") || system.contains("aix") -> 12 | Platform.Linux 13 | 14 | system.contains("mac") -> 15 | Platform.MacOS 16 | 17 | else -> 18 | Platform.Linux 19 | } 20 | } 21 | } 22 | 23 | internal enum class Platform { 24 | Linux, 25 | MacOS, 26 | Windows 27 | } 28 | -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/platform/windows/win32/Ole32.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.platform.windows.win32 2 | 3 | import com.sun.jna.Native 4 | import com.sun.jna.Pointer 5 | 6 | internal object Ole32 { 7 | init { 8 | Native.register("ole32") 9 | } 10 | 11 | external fun OleInitialize(pvReserved: Pointer?): Pointer? 12 | external fun CoTaskMemFree(pv: Pointer?) 13 | } 14 | -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/com/mohamedrejeb/calf/picker/platform/windows/win32/Shell32.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker.platform.windows.win32 2 | 3 | import com.sun.jna.Native 4 | import com.sun.jna.Pointer 5 | import com.sun.jna.Structure 6 | 7 | internal object Shell32 { 8 | init { 9 | Native.register("shell32") 10 | } 11 | 12 | external fun SHBrowseForFolder(params: BrowseInfo?): Pointer? 13 | external fun SHGetPathFromIDListW(pidl: Pointer?, path: Pointer?): Boolean 14 | 15 | // flags for the BrowseInfo structure 16 | const val BIF_RETURNONLYFSDIRS: Int = 0x00000001 17 | const val BIF_DONTGOBELOWDOMAIN: Int = 0x00000002 18 | const val BIF_NEWDIALOGSTYLE: Int = 0x00000040 19 | const val BIF_EDITBOX: Int = 0x00000010 20 | const val BIF_USENEWUI: Int = BIF_EDITBOX or BIF_NEWDIALOGSTYLE 21 | const val BIF_NONEWFOLDERBUTTON: Int = 0x00000200 22 | const val BIF_BROWSEINCLUDEFILES: Int = 0x00004000 23 | const val BIF_SHAREABLE: Int = 0x00008000 24 | const val BIF_BROWSEFILEJUNCTIONS: Int = 0x00010000 25 | 26 | // http://msdn.microsoft.com/en-us/library/bb773205.aspx 27 | class BrowseInfo : Structure() { 28 | @JvmField var hwndOwner: Pointer? = null 29 | @JvmField var pidlRoot: Pointer? = null 30 | @JvmField var pszDisplayName: String? = null 31 | @JvmField var lpszTitle: String? = null 32 | @JvmField var ulFlags: Int = 0 33 | @JvmField var lpfn: Pointer? = null 34 | @JvmField var lParam: Pointer? = null 35 | @JvmField var iImage: Int = 0 36 | 37 | override fun getFieldOrder(): List { 38 | return listOf( 39 | "hwndOwner", 40 | "pidlRoot", 41 | "pszDisplayName", 42 | "lpszTitle", 43 | "ulFlags", 44 | "lpfn", 45 | "lParam", 46 | "iImage" 47 | ) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /calf-file-picker/src/desktopMain/kotlin/jodd/io/IOUtil.kt: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2003-present, Jodd Team (http://jodd.org) 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | package jodd.io 26 | 27 | import java.io.Closeable 28 | import java.io.Flushable 29 | import java.io.IOException 30 | 31 | /** 32 | * Optimized byte and character stream utilities. 33 | */ 34 | object IOUtil { 35 | // ---------------------------------------------------------------- silent close 36 | /** 37 | * Closes silently the closable object. If it is [Flushable], it 38 | * will be flushed first. No exception will be thrown if an I/O error occurs. 39 | */ 40 | fun close(closeable: Closeable?) { 41 | if (closeable == null) 42 | return 43 | 44 | if (closeable is Flushable) { 45 | try { 46 | closeable.flush() 47 | } catch (ignored: IOException) { 48 | } 49 | } 50 | 51 | try { 52 | closeable.close() 53 | } catch (ignored: IOException) { 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /calf-file-picker/src/iosMain/kotlin/com.mohamedrejeb.calf.picker/ComposeResource.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import androidx.compose.ui.graphics.toComposeImageBitmap 5 | import org.jetbrains.skia.Image 6 | 7 | actual fun ByteArray.toImageBitmap(): ImageBitmap = 8 | Image.makeFromEncoded(this).toComposeImageBitmap() -------------------------------------------------------------------------------- /calf-file-picker/src/iosMain/kotlin/com.mohamedrejeb.calf.picker/TempFile.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import platform.Foundation.NSData 4 | import platform.Foundation.NSTemporaryDirectory 5 | import platform.Foundation.NSURL 6 | import platform.Foundation.NSUUID 7 | import platform.Foundation.dataWithContentsOfURL 8 | import platform.Foundation.writeToURL 9 | 10 | internal fun NSURL.createTempFile(): NSURL? { 11 | val extension = absoluteString 12 | ?.substringAfterLast('/') 13 | ?.substringAfterLast('.', "") ?: return null 14 | val data = NSData.dataWithContentsOfURL(this) 15 | ?: absoluteURL?.dataRepresentation() 16 | ?: return null 17 | return NSURL.fileURLWithPath("${NSTemporaryDirectory()}/${NSUUID().UUIDString}.$extension").apply { 18 | data.writeToURL(this, true) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /calf-file-picker/src/jsMain/kotlin/com.mohamedrejeb.calf.picker/ComposeResource.js.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import androidx.compose.ui.graphics.toComposeImageBitmap 5 | import org.jetbrains.skia.Image 6 | 7 | actual fun ByteArray.toImageBitmap(): ImageBitmap = 8 | Image.makeFromEncoded(this).toComposeImageBitmap() -------------------------------------------------------------------------------- /calf-file-picker/src/wasmJsMain/kotlin/com.mohamedrejeb.calf/picker/ComposeResource.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.picker 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import androidx.compose.ui.graphics.toComposeImageBitmap 5 | import org.jetbrains.skia.Image 6 | 7 | actual fun ByteArray.toImageBitmap(): ImageBitmap = Image.makeFromEncoded(this).toComposeImageBitmap() 8 | -------------------------------------------------------------------------------- /calf-geo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | } 4 | 5 | kotlin { 6 | sourceSets.commonMain.dependencies { 7 | implementation(compose.runtime) 8 | implementation(compose.foundation) 9 | } 10 | sourceSets.androidMain.dependencies { 11 | implementation(libs.appcompat) 12 | implementation(libs.lifecycle.extensions) 13 | implementation(libs.play.services.location) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /calf-geo/src/commonMain/kotlin/com.mohamedrejeb.calf/geo/Altitude.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | data class Altitude( 4 | val altitudeMeters: Double, 5 | val altitudeAccuracyMeters: Double? 6 | ) -------------------------------------------------------------------------------- /calf-geo/src/commonMain/kotlin/com.mohamedrejeb.calf/geo/Azimuth.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | data class Azimuth( 4 | val azimuthDegrees: Double, 5 | val azimuthAccuracyDegrees: Double? 6 | ) -------------------------------------------------------------------------------- /calf-geo/src/commonMain/kotlin/com.mohamedrejeb.calf/geo/ExtendedLocation.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | data class ExtendedLocation( 4 | val location: Location, 5 | val azimuth: Azimuth, 6 | val speed: Speed, 7 | val altitude: Altitude, 8 | val timestampMs: Long 9 | ) -------------------------------------------------------------------------------- /calf-geo/src/commonMain/kotlin/com.mohamedrejeb.calf/geo/LatLng.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | import kotlin.math.* 4 | 5 | /** 6 | * A class representing a pair of latitude and longitude coordinates, stored as degrees. 7 | * 8 | * @property latitude The latitude in degrees. 9 | * @property longitude The longitude in degrees. 10 | */ 11 | data class LatLng( 12 | val latitude: Double, 13 | val longitude: Double 14 | ) { 15 | 16 | /** 17 | * Returns the distance to the given [LatLng] in kilometers. 18 | * 19 | * @param latLng The [LatLng] to calculate the distance to. 20 | * @return The distance to the given [LatLng] in kilometers. 21 | */ 22 | fun distanceTo(latLng: LatLng): Double { 23 | val lat1 = latitude 24 | val lon1 = longitude 25 | val lat2 = latLng.latitude 26 | val lon2 = latLng.longitude 27 | 28 | val r = EarthRadius 29 | val dLat = toRadians(lat2 - lat1) 30 | val dLon = toRadians(lon2 - lon1) 31 | val a = sin(dLat / 2) * sin(dLat / 2) + 32 | cos(toRadians(lat1)) * cos(toRadians(lat2)) * sin(dLon / 2) * sin(dLon / 2) 33 | val c = 2 * asin(sqrt(a)) 34 | return r * c 35 | } 36 | 37 | /** 38 | * Returns the angle to the given [LatLng] in degrees. 39 | * 40 | * @param latLng The [LatLng] to calculate the angle to. 41 | * @param rounded Whether the angle should be rounded to the nearest 10 degrees. 42 | * @return The angle to the given [LatLng] in degrees. 43 | */ 44 | fun getAngleTo(latLng: LatLng, rounded: Boolean = true): Double { 45 | val lat1 = toRadians(this.latitude) 46 | val lat2 = toRadians(latLng.latitude) 47 | val lon1 = toRadians(this.longitude) 48 | val lon2 = toRadians(latLng.longitude) 49 | 50 | val dLon = lon2 - lon1 51 | 52 | val y = sin(dLon) * cos(lat2) 53 | val x = cos(lat1) * sin(lat2) - 54 | (sin(lat1) * cos(lat2) * cos(dLon)) 55 | 56 | var brng = atan2(y, x) 57 | 58 | brng = toDegree(brng) 59 | brng = (brng + 360) % 360 60 | 61 | var angle = brng 62 | if (rounded) { 63 | angle = (round(brng / 10) * 10) 64 | } 65 | if (angle == 360.0) angle = 0.0 66 | 67 | return angle 68 | } 69 | 70 | private fun toRadians(angle: Double): Double { 71 | return angle * PI / 180.0 72 | } 73 | 74 | private fun toDegree(angle: Double): Double { 75 | return angle * 180.0 / PI 76 | } 77 | 78 | private companion object { 79 | const val EarthRadius = 6371.00 // in Kilometers 80 | } 81 | } -------------------------------------------------------------------------------- /calf-geo/src/commonMain/kotlin/com.mohamedrejeb.calf/geo/Location.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | /** 4 | * A class representing the coordinates of a location, stored as a [LatLng] and an accuracy in meters. 5 | * 6 | * @property coordinates The coordinates of the location. 7 | * @property coordinatesAccuracyMeters The accuracy of the coordinates in meters. 8 | */ 9 | data class Location( 10 | val coordinates: LatLng, 11 | val coordinatesAccuracyMeters: Double 12 | ) -------------------------------------------------------------------------------- /calf-geo/src/commonMain/kotlin/com.mohamedrejeb.calf/geo/LocationTracker.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | 6 | expect class LocationTracker { 7 | // val permissionsController: PermissionsController 8 | 9 | suspend fun startTracking() // can be suspended for request permission 10 | fun stopTracking() 11 | 12 | val locationState: State 13 | 14 | val extendedLocationState: State 15 | } -------------------------------------------------------------------------------- /calf-geo/src/commonMain/kotlin/com.mohamedrejeb.calf/geo/Speed.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | data class Speed( 4 | val speedMps: Double, 5 | val speedAccuracyMps: Double? 6 | ) -------------------------------------------------------------------------------- /calf-geo/src/desktopMain/kotlin/com/mohamedrejeb/calf/geo/LocationTracker.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | 7 | actual class LocationTracker { 8 | actual suspend fun startTracking() { 9 | println("startTracking") 10 | } 11 | 12 | actual fun stopTracking() { 13 | println("stopTracking") 14 | } 15 | 16 | private val _locationState: MutableState = mutableStateOf(LatLng(0.0, 0.0)) 17 | actual val locationState: State = _locationState 18 | 19 | private val _extendedLocationState = mutableStateOf( 20 | ExtendedLocation( 21 | location = Location( 22 | coordinates = LatLng(0.0, 0.0), 23 | coordinatesAccuracyMeters = 0.0 24 | ), 25 | azimuth = Azimuth( 26 | azimuthDegrees = 0.0, 27 | azimuthAccuracyDegrees = null 28 | ), 29 | speed = Speed( 30 | speedMps = 0.0, 31 | speedAccuracyMps = null 32 | ), 33 | altitude = Altitude( 34 | altitudeMeters = 0.0, 35 | altitudeAccuracyMeters = null 36 | ), 37 | timestampMs = 0L 38 | ) 39 | ) 40 | actual val extendedLocationState: State = _extendedLocationState 41 | } -------------------------------------------------------------------------------- /calf-geo/src/iosMain/kotlin/com.mohamedrejeb.calf/geo/LocationTracker.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | 7 | actual class LocationTracker { 8 | actual suspend fun startTracking() { 9 | println("startTracking") 10 | } 11 | 12 | actual fun stopTracking() { 13 | println("stopTracking") 14 | } 15 | 16 | private val _locationState: MutableState = mutableStateOf(LatLng(0.0, 0.0)) 17 | actual val locationState: State = _locationState 18 | 19 | private val _extendedLocationState = mutableStateOf( 20 | ExtendedLocation( 21 | location = Location( 22 | coordinates = LatLng(0.0, 0.0), 23 | coordinatesAccuracyMeters = 0.0 24 | ), 25 | azimuth = Azimuth( 26 | azimuthDegrees = 0.0, 27 | azimuthAccuracyDegrees = null 28 | ), 29 | speed = Speed( 30 | speedMps = 0.0, 31 | speedAccuracyMps = null 32 | ), 33 | altitude = Altitude( 34 | altitudeMeters = 0.0, 35 | altitudeAccuracyMeters = null 36 | ), 37 | timestampMs = 0L 38 | ) 39 | ) 40 | actual val extendedLocationState: State = _extendedLocationState 41 | } -------------------------------------------------------------------------------- /calf-geo/src/jsMain/kotlin/com.mohamedrejeb.calf/geo/LocationTracker.js.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | 7 | actual class LocationTracker { 8 | actual suspend fun startTracking() { 9 | println("startTracking") 10 | } 11 | 12 | actual fun stopTracking() { 13 | println("stopTracking") 14 | } 15 | 16 | private val _locationState: MutableState = mutableStateOf(LatLng(0.0, 0.0)) 17 | actual val locationState: State = _locationState 18 | 19 | private val _extendedLocationState = mutableStateOf( 20 | ExtendedLocation( 21 | location = Location( 22 | coordinates = LatLng(0.0, 0.0), 23 | coordinatesAccuracyMeters = 0.0 24 | ), 25 | azimuth = Azimuth( 26 | azimuthDegrees = 0.0, 27 | azimuthAccuracyDegrees = null 28 | ), 29 | speed = Speed( 30 | speedMps = 0.0, 31 | speedAccuracyMps = null 32 | ), 33 | altitude = Altitude( 34 | altitudeMeters = 0.0, 35 | altitudeAccuracyMeters = null 36 | ), 37 | timestampMs = 0L 38 | ) 39 | ) 40 | actual val extendedLocationState: State = _extendedLocationState 41 | } -------------------------------------------------------------------------------- /calf-geo/src/wasmJsMain/kotlin/com.mohamedrejeb.calf/geo/LocationTracker.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.geo 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | 7 | actual class LocationTracker { 8 | actual suspend fun startTracking() { 9 | println("startTracking") 10 | } 11 | 12 | actual fun stopTracking() { 13 | println("stopTracking") 14 | } 15 | 16 | private val _locationState: MutableState = mutableStateOf(LatLng(0.0, 0.0)) 17 | actual val locationState: State = _locationState 18 | 19 | private val _extendedLocationState = mutableStateOf( 20 | ExtendedLocation( 21 | location = Location( 22 | coordinates = LatLng(0.0, 0.0), 23 | coordinatesAccuracyMeters = 0.0 24 | ), 25 | azimuth = Azimuth( 26 | azimuthDegrees = 0.0, 27 | azimuthAccuracyDegrees = null 28 | ), 29 | speed = Speed( 30 | speedMps = 0.0, 31 | speedAccuracyMps = null 32 | ), 33 | altitude = Altitude( 34 | altitudeMeters = 0.0, 35 | altitudeAccuracyMeters = null 36 | ), 37 | timestampMs = 0L 38 | ) 39 | ) 40 | actual val extendedLocationState: State = _extendedLocationState 41 | } -------------------------------------------------------------------------------- /calf-io/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kotlin.multiplatform") 3 | id("module.publication") 4 | } 5 | 6 | kotlin { 7 | sourceSets.commonMain.dependencies { 8 | api(projects.calfCore) 9 | } 10 | 11 | sourceSets.androidMain.dependencies { 12 | implementation(libs.documentfile) 13 | } 14 | 15 | sourceSets.wasmJsMain.dependencies { 16 | implementation(libs.kotlinx.browser) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /calf-io/src/androidMain/kotlin/com/mohamedrejeb/calf/io/KmpFile.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.io 2 | 3 | import android.net.Uri 4 | import androidx.documentfile.provider.DocumentFile 5 | import com.mohamedrejeb.calf.core.PlatformContext 6 | import java.io.FileNotFoundException 7 | 8 | actual class KmpFile( 9 | val uri: Uri, 10 | ) 11 | 12 | actual fun KmpFile.exists(context: PlatformContext): Boolean = 13 | try { 14 | val inputStream = context.contentResolver.openInputStream(uri)!! 15 | inputStream.close() 16 | 17 | true 18 | } catch (e: Exception) { 19 | false 20 | } 21 | 22 | /** 23 | * Reads the content of the file as a byte array 24 | * 25 | * @param context the context to use to open the file 26 | * @throws FileNotFoundException if the file is not found 27 | * @return the content of the file as a byte array 28 | */ 29 | @Throws(FileNotFoundException::class) 30 | actual suspend fun KmpFile.readByteArray(context: PlatformContext): ByteArray { 31 | val inputStream = 32 | context.contentResolver.openInputStream(uri) 33 | ?: throw FileNotFoundException("File not found") 34 | 35 | return inputStream.readBytes().also { 36 | inputStream.close() 37 | } 38 | } 39 | 40 | actual fun KmpFile.getName(context: PlatformContext): String? { 41 | val documentFile = DocumentFile.fromSingleUri(context, uri) 42 | 43 | return documentFile?.name 44 | } 45 | 46 | actual fun KmpFile.getPath(context: PlatformContext): String? = uri.toString() 47 | 48 | actual fun KmpFile.isDirectory(context: PlatformContext): Boolean { 49 | val documentFile = DocumentFile.fromSingleUri(context, uri) 50 | 51 | return documentFile?.isDirectory == true 52 | } 53 | -------------------------------------------------------------------------------- /calf-io/src/commonMain/kotlin/com.mohamedrejeb.calf/io/KmpFile.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.io 2 | 3 | import com.mohamedrejeb.calf.core.PlatformContext 4 | 5 | /** 6 | * A wrapper class for a file in the platform specific implementation. 7 | */ 8 | expect class KmpFile 9 | 10 | /** 11 | * Checks if the KmpFile exists in the specified platform context. 12 | * 13 | * @param context The platform context in which to check the existence of the file. 14 | * @return True if the file exists, false otherwise. 15 | */ 16 | expect fun KmpFile.exists(context: PlatformContext): Boolean 17 | 18 | /** 19 | * Reads the content of the KmpFile as a byte array. 20 | * 21 | * @param context The platform context. 22 | * @return The content of the file as a byte array. 23 | */ 24 | expect suspend fun KmpFile.readByteArray(context: PlatformContext): ByteArray 25 | 26 | /** 27 | * Reads the name of the KmpFile. 28 | * 29 | * @param context The platform context. 30 | * @return The name of the file as a string. 31 | */ 32 | expect fun KmpFile.getName(context: PlatformContext): String? 33 | 34 | /** 35 | * Reads the path of the KmpFile. 36 | * 37 | * @param context The platform context. 38 | * @return The path of the file as a string. 39 | */ 40 | expect fun KmpFile.getPath(context: PlatformContext): String? 41 | 42 | /** 43 | * Checks if the KmpFile is a directory. 44 | * 45 | * @param context The platform context. 46 | * @return True if the file is a directory, false otherwise. 47 | */ 48 | expect fun KmpFile.isDirectory(context: PlatformContext): Boolean 49 | 50 | /** 51 | * Checks if the KmpFile is a file. 52 | * 53 | * @param context The platform context. 54 | * @return True if the file is a file, false otherwise. 55 | */ 56 | fun KmpFile.isFile(context: PlatformContext): Boolean = !isDirectory(context) 57 | -------------------------------------------------------------------------------- /calf-io/src/desktopMain/kotlin/com/mohamedrejeb/calf/io/KmpFile.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.io 2 | 3 | import com.mohamedrejeb.calf.core.PlatformContext 4 | import java.io.File 5 | 6 | actual class KmpFile( 7 | val file: File, 8 | ) 9 | 10 | actual fun KmpFile.exists(context: PlatformContext) = file.exists() 11 | 12 | actual suspend fun KmpFile.readByteArray(context: PlatformContext): ByteArray = file.readBytes() 13 | 14 | actual fun KmpFile.getName(context: PlatformContext): String? = file.name 15 | 16 | actual fun KmpFile.getPath(context: PlatformContext): String? = file.path 17 | 18 | actual fun KmpFile.isDirectory(context: PlatformContext): Boolean = file.isDirectory 19 | -------------------------------------------------------------------------------- /calf-io/src/iosMain/kotlin/com.mohamedrejeb.calf/io/KmpFile.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.io 2 | 3 | import com.mohamedrejeb.calf.core.InternalCalfApi 4 | import com.mohamedrejeb.calf.core.PlatformContext 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import kotlinx.cinterop.addressOf 7 | import kotlinx.cinterop.usePinned 8 | import platform.Foundation.NSData 9 | import platform.Foundation.NSFileManager 10 | import platform.Foundation.NSURL 11 | import platform.Foundation.NSURLIsDirectoryKey 12 | import platform.Foundation.dataWithContentsOfURL 13 | import platform.posix.memcpy 14 | 15 | /** 16 | * A wrapper class for a file in the platform-specific implementation. 17 | * 18 | * @property url The URL of the file. 19 | * @property tempUrl The temporary URL of the file, 20 | * this is used to read the content of the file outside the file picker callback. 21 | */ 22 | actual class KmpFile @InternalCalfApi constructor( 23 | val url: NSURL, 24 | internal val tempUrl: NSURL, 25 | ) { 26 | @OptIn(InternalCalfApi::class) 27 | constructor(url: NSURL) : this(url, url) 28 | } 29 | 30 | actual fun KmpFile.exists(context: PlatformContext): Boolean { 31 | return NSFileManager.defaultManager.fileExistsAtPath(url.path ?: return false) 32 | } 33 | 34 | @OptIn(ExperimentalForeignApi::class) 35 | actual suspend fun KmpFile.readByteArray(context: PlatformContext): ByteArray { 36 | val data = NSData.dataWithContentsOfURL(tempUrl) ?: return ByteArray(0) 37 | val byteArraySize: Int = if (data.length > Int.MAX_VALUE.toUInt()) Int.MAX_VALUE else data.length.toInt() 38 | return ByteArray(byteArraySize).apply { 39 | usePinned { 40 | memcpy(it.addressOf(0), data.bytes, data.length) 41 | } 42 | } 43 | } 44 | 45 | actual fun KmpFile.getName(context: PlatformContext): String? = 46 | url.absoluteString 47 | ?.removeSuffix("/") 48 | ?.split('/') 49 | ?.lastOrNull() 50 | 51 | actual fun KmpFile.getPath(context: PlatformContext): String? = 52 | url.absoluteString 53 | 54 | @OptIn(ExperimentalForeignApi::class) 55 | actual fun KmpFile.isDirectory(context: PlatformContext): Boolean { 56 | val result = url.resourceValuesForKeys(listOf(NSURLIsDirectoryKey), error = null) 57 | return result?.get(NSURLIsDirectoryKey) == true 58 | } 59 | -------------------------------------------------------------------------------- /calf-io/src/jsMain/kotlin/com.mohamedrejeb.calf/io/KmpFile.js.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.io 2 | 3 | import com.mohamedrejeb.calf.core.PlatformContext 4 | import org.khronos.webgl.ArrayBuffer 5 | import org.khronos.webgl.Uint8Array 6 | import org.khronos.webgl.get 7 | import org.w3c.files.File 8 | import org.w3c.files.FileReader 9 | import kotlin.coroutines.resume 10 | import kotlin.coroutines.suspendCoroutine 11 | 12 | actual class KmpFile( 13 | val file: File, 14 | ) 15 | 16 | actual fun KmpFile.exists(context: PlatformContext) = true 17 | 18 | actual suspend fun KmpFile.readByteArray(context: PlatformContext): ByteArray = 19 | suspendCoroutine { continuation -> 20 | val fileReader = FileReader() 21 | fileReader.readAsArrayBuffer(file) 22 | fileReader.onloadend = { event -> 23 | if (event.target.asDynamic().readyState == FileReader.DONE) { 24 | val arrayBuffer: ArrayBuffer = event.target.asDynamic().result 25 | val array = Uint8Array(arrayBuffer) 26 | val byteArray = 27 | ByteArray(array.length) { index -> 28 | array[index] 29 | } 30 | continuation.resume(byteArray) 31 | } else { 32 | continuation.resume(ByteArray(0)) 33 | } 34 | } 35 | } 36 | 37 | actual fun KmpFile.getName(context: PlatformContext): String? = file.name 38 | 39 | actual fun KmpFile.getPath(context: PlatformContext): String? = file.name 40 | 41 | actual fun KmpFile.isDirectory(context: PlatformContext): Boolean = !file.name.contains(".") 42 | -------------------------------------------------------------------------------- /calf-io/src/wasmJsMain/kotlin/com.mohamedrejeb.calf/io/KmpFile.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.io 2 | 3 | import com.mohamedrejeb.calf.core.PlatformContext 4 | import org.khronos.webgl.ArrayBuffer 5 | import org.khronos.webgl.Uint8Array 6 | import org.khronos.webgl.get 7 | import org.w3c.dom.events.Event 8 | import org.w3c.files.File 9 | import org.w3c.files.FileReader 10 | import kotlin.coroutines.resume 11 | import kotlin.coroutines.suspendCoroutine 12 | 13 | actual class KmpFile( 14 | val file: File, 15 | ) 16 | 17 | actual fun KmpFile.exists(context: PlatformContext) = true 18 | 19 | actual suspend fun KmpFile.readByteArray(context: PlatformContext): ByteArray = 20 | suspendCoroutine { continuation -> 21 | val fileReader = FileReader() 22 | fileReader.readAsArrayBuffer(file) 23 | fileReader.onloadend = { event -> 24 | val readyState = getReadyState(event) 25 | if (readyState == FileReader.DONE) { 26 | val arrayBuffer: ArrayBuffer = getResult(event) 27 | val array = Uint8Array(arrayBuffer) 28 | val byteArray = 29 | ByteArray(array.length) { index -> 30 | array[index] 31 | } 32 | continuation.resume(byteArray) 33 | } else { 34 | continuation.resume(ByteArray(0)) 35 | } 36 | } 37 | } 38 | 39 | actual fun KmpFile.getName(context: PlatformContext): String? = file.name 40 | 41 | actual fun KmpFile.getPath(context: PlatformContext): String? = file.name 42 | 43 | actual fun KmpFile.isDirectory(context: PlatformContext): Boolean = !file.name.contains(".") 44 | 45 | private fun getReadyState(event: Event): Short = js("event.target.readyState") 46 | 47 | private fun getResult(event: Event): ArrayBuffer = js("event.target.result") 48 | -------------------------------------------------------------------------------- /calf-maps/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | } 4 | 5 | kotlin { 6 | sourceSets.commonMain.dependencies { 7 | implementation(compose.runtime) 8 | implementation(compose.foundation) 9 | implementation(compose.material) 10 | } 11 | sourceSets.commonTest.dependencies { 12 | implementation(libs.kotlin.test) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /calf-media/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinMultiplatform) 3 | alias(libs.plugins.composeCompiler) 4 | alias(libs.plugins.composeMultiplatform) 5 | alias(libs.plugins.androidLibrary) 6 | } 7 | 8 | kotlin { 9 | kotlin.applyDefaultHierarchyTemplate() 10 | androidTarget { 11 | publishLibraryVariants("release") 12 | compilations.all { 13 | kotlinOptions { 14 | jvmTarget = "11" 15 | } 16 | } 17 | } 18 | jvm("desktop") { 19 | jvmToolchain(11) 20 | } 21 | // js(IR) { 22 | // browser() 23 | // } 24 | iosX64() 25 | iosArm64() 26 | iosSimulatorArm64() 27 | 28 | sourceSets.commonMain.dependencies { 29 | implementation(compose.runtime) 30 | implementation(compose.foundation) 31 | implementation(compose.material) 32 | } 33 | sourceSets.commonTest.dependencies { 34 | implementation(libs.kotlin.test) 35 | } 36 | } 37 | 38 | android { 39 | namespace = "com.mohamedrejeb.calf.sf.symbols" 40 | compileSdk = libs.versions.android.compileSdk.get().toInt() 41 | defaultConfig { 42 | minSdk = libs.versions.android.minSdk.get().toInt() 43 | } 44 | compileOptions { 45 | sourceCompatibility = JavaVersion.VERSION_11 46 | targetCompatibility = JavaVersion.VERSION_11 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /calf-navigation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | } 4 | 5 | kotlin { 6 | sourceSets.commonMain.dependencies { 7 | implementation(compose.runtime) 8 | implementation(compose.foundation) 9 | implementation(compose.material) 10 | } 11 | 12 | sourceSets.androidMain.dependencies { 13 | implementation(libs.navigation.compose) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /calf-navigation/src/androidMain/kotlin/com/mohamedrejeb/calf/navigation/AndroidBackHandler.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun AndroidBackHandler( 8 | enabled: Boolean, 9 | onBack: () -> Unit 10 | ) { 11 | BackHandler(enabled = enabled) { 12 | onBack() 13 | } 14 | } -------------------------------------------------------------------------------- /calf-navigation/src/androidMain/kotlin/com/mohamedrejeb/calf/navigation/NavHostController.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.navigation.NavHostController 4 | 5 | actual typealias NavHostController = NavHostController 6 | 7 | actual fun NavHostController.navigate(route: String) { 8 | navigate(route) 9 | } 10 | 11 | actual fun NavHostController.popBackStack(): Boolean { 12 | return popBackStack() 13 | } -------------------------------------------------------------------------------- /calf-navigation/src/commonMain/kotlin/com.mohamedrejeb.calf/navigation/AdaptiveBundle.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | /** 4 | * A mapping from String keys to various [Parcelable] values. 5 | * 6 | * 7 | * **Warning:** Note that [AdaptiveBundle] is a lazy container and as such it does NOT implement 8 | * [.equals] or [.hashCode]. 9 | * 10 | * @see PersistableBundle 11 | */ 12 | class AdaptiveBundle : AdaptiveBaseBundle() -------------------------------------------------------------------------------- /calf-navigation/src/commonMain/kotlin/com.mohamedrejeb.calf/navigation/AdaptiveNavHost.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | 9 | @Composable 10 | fun AdaptiveNavHost( 11 | navController: AdaptiveNavHostController, 12 | startDestination: String, 13 | modifier: Modifier = Modifier, 14 | builder: NavGraphBuilder.() -> Unit 15 | ) { 16 | AndroidBackHandler(enabled = navController.backStack.size > 1) { 17 | navController.popBackStack() 18 | } 19 | 20 | val graphBuilder = remember { 21 | NavGraphBuilder().also { 22 | it.builder() 23 | } 24 | } 25 | 26 | LaunchedEffect(Unit) { 27 | if (navController.currentDestination == null) 28 | navController.navigate(startDestination) 29 | } 30 | 31 | Box(modifier = modifier) { 32 | navController.currentDestination?.let { currentDestination -> 33 | graphBuilder.destinations.find { it.route == currentDestination }?.let { destination -> 34 | destination.content(destination.arguments) 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /calf-navigation/src/commonMain/kotlin/com.mohamedrejeb.calf/navigation/AdaptiveNavHostController.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.mutableStateListOf 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.saveable.rememberSaveable 7 | 8 | @Composable 9 | fun rememberNavController(): AdaptiveNavHostController { 10 | return rememberSaveable(saver = AdaptiveNavHostController.Saver()) { 11 | AdaptiveNavHostController() 12 | } 13 | } 14 | 15 | class AdaptiveNavHostController { 16 | val backStack = mutableStateListOf() 17 | val currentDestination: String? 18 | get() = backStack.lastOrNull() 19 | 20 | fun navigate( 21 | route: String, 22 | arguments: Map = emptyMap() 23 | ) { 24 | backStack.add(route) 25 | } 26 | 27 | fun popBackStack(): Boolean { 28 | if (backStack.size <= 1) return false 29 | 30 | return backStack.removeLastOrNull() != null 31 | } 32 | 33 | companion object { 34 | fun Saver(): Saver { 35 | return Saver( 36 | save = { 37 | it.backStack.toTypedArray() 38 | }, 39 | restore = { 40 | AdaptiveNavHostController().apply { backStack.addAll(it) } 41 | } 42 | ) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /calf-navigation/src/commonMain/kotlin/com.mohamedrejeb.calf/navigation/AndroidBackHandler.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | expect fun AndroidBackHandler( 7 | enabled: Boolean = true, 8 | onBack: () -> Unit 9 | ) -------------------------------------------------------------------------------- /calf-navigation/src/commonMain/kotlin/com.mohamedrejeb.calf/navigation/NavDestination.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | class NavDestination( 6 | val route: String, 7 | val arguments: Map = emptyMap(), 8 | val content: @Composable (arguments: Map) -> Unit, 9 | ) -------------------------------------------------------------------------------- /calf-navigation/src/commonMain/kotlin/com.mohamedrejeb.calf/navigation/NavGraphBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | public open class NavGraphBuilder { 6 | internal val destinations = mutableListOf() 7 | 8 | /** 9 | * Add the destination to the [NavGraphBuilder] 10 | */ 11 | public fun addDestination(destination: NavDestination) { 12 | destinations += destination 13 | } 14 | 15 | public fun composable( 16 | route: String, 17 | arguments: Map = emptyMap(), 18 | content: @Composable (arguments: Map) -> Unit 19 | ) { 20 | addDestination(NavDestination(route, arguments, content)) 21 | } 22 | } -------------------------------------------------------------------------------- /calf-navigation/src/commonMain/kotlin/com.mohamedrejeb.calf/navigation/NavHostController.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | expect class NavHostController 4 | 5 | expect fun NavHostController.navigate(route: String) 6 | 7 | expect fun NavHostController.popBackStack(): Boolean -------------------------------------------------------------------------------- /calf-navigation/src/desktopMain/kotlin/com/mohamedrejeb/calf/navigation/AndroidBackHandler.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | actual fun AndroidBackHandler( 7 | enabled: Boolean, 8 | onBack: () -> Unit 9 | ) { 10 | // This is a no-op on desktop 11 | } -------------------------------------------------------------------------------- /calf-navigation/src/desktopMain/kotlin/com/mohamedrejeb/calf/navigation/NavHostController.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | actual typealias NavHostController = AdaptiveNavHostController 4 | 5 | actual fun NavHostController.navigate(route: String) { 6 | navigate(route) 7 | } 8 | 9 | actual fun NavHostController.popBackStack(): Boolean { 10 | return popBackStack() 11 | } -------------------------------------------------------------------------------- /calf-navigation/src/iosMain/kotlin/com.mohamedrejeb.calf/navigation/AndroidBackHandler.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | actual fun AndroidBackHandler( 7 | enabled: Boolean, 8 | onBack: () -> Unit 9 | ) { 10 | // This is a no-op on iOS 11 | } -------------------------------------------------------------------------------- /calf-navigation/src/iosMain/kotlin/com.mohamedrejeb.calf/navigation/NavHostController.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | actual typealias NavHostController = AdaptiveNavHostController 4 | 5 | actual fun NavHostController.navigate(route: String) { 6 | navigate(route) 7 | } 8 | 9 | actual fun NavHostController.popBackStack(): Boolean { 10 | return popBackStack() 11 | } -------------------------------------------------------------------------------- /calf-navigation/src/jsMain/kotlin/com.mohamedrejeb.calf/navigation/AndroidBackHandler.js.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | actual fun AndroidBackHandler( 7 | enabled: Boolean, 8 | onBack: () -> Unit 9 | ) { 10 | // This is a no-op on web 11 | } -------------------------------------------------------------------------------- /calf-navigation/src/jsMain/kotlin/com.mohamedrejeb.calf/navigation/NavHostController.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | actual typealias NavHostController = AdaptiveNavHostController 4 | 5 | actual fun NavHostController.navigate(route: String) { 6 | navigate(route) 7 | } 8 | 9 | actual fun NavHostController.popBackStack(): Boolean { 10 | return popBackStack() 11 | } -------------------------------------------------------------------------------- /calf-navigation/src/wasmJsMain/kotlin/com.mohamedrejeb.calf/navigation/AndroidBackHandler.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | actual fun AndroidBackHandler( 7 | enabled: Boolean, 8 | onBack: () -> Unit, 9 | ) { 10 | // This is a no-op on web 11 | } 12 | -------------------------------------------------------------------------------- /calf-navigation/src/wasmJsMain/kotlin/com.mohamedrejeb.calf/navigation/NavHostController.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.navigation 2 | 3 | actual typealias NavHostController = AdaptiveNavHostController 4 | 5 | actual fun NavHostController.navigate(route: String) { 6 | navigate(route) 7 | } 8 | 9 | actual fun NavHostController.popBackStack(): Boolean { 10 | return popBackStack() 11 | } 12 | -------------------------------------------------------------------------------- /calf-notifications/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinMultiplatform) 3 | alias(libs.plugins.composeCompiler) 4 | alias(libs.plugins.composeMultiplatform) 5 | alias(libs.plugins.androidLibrary) 6 | } 7 | 8 | kotlin { 9 | kotlin.applyDefaultHierarchyTemplate() 10 | androidTarget { 11 | publishLibraryVariants("release") 12 | compilations.all { 13 | kotlinOptions { 14 | jvmTarget = "11" 15 | } 16 | } 17 | } 18 | jvm("desktop") { 19 | jvmToolchain(11) 20 | } 21 | // js(IR) { 22 | // browser() 23 | // } 24 | iosX64() 25 | iosArm64() 26 | iosSimulatorArm64() 27 | 28 | sourceSets.commonMain.dependencies { 29 | implementation(compose.runtime) 30 | implementation(compose.foundation) 31 | implementation(compose.material) 32 | } 33 | sourceSets.commonTest.dependencies { 34 | implementation(libs.kotlin.test) 35 | } 36 | } 37 | 38 | android { 39 | namespace = "com.mohamedrejeb.calf.sf.symbols" 40 | compileSdk = libs.versions.android.compileSdk.get().toInt() 41 | defaultConfig { 42 | minSdk = libs.versions.android.minSdk.get().toInt() 43 | } 44 | compileOptions { 45 | sourceCompatibility = JavaVersion.VERSION_11 46 | targetCompatibility = JavaVersion.VERSION_11 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /calf-notifications/src/iosMain/kotlin/com.mohamedrejeb.calf/notifications/Main.kt: -------------------------------------------------------------------------------- 1 | fun main() { 2 | println("LaunchedEffect") 3 | val notif = UNUserNotificationCenter.currentNotificationCenter() 4 | notif.requestAuthorizationWithOptions( 5 | options = UNAuthorizationOptionAlert or UNAuthorizationOptionBadge or UNAuthorizationOptionSound, 6 | completionHandler = { granted, error -> 7 | if (granted) { 8 | scope.launch(Dispatchers.Main) { 9 | val content = UNMutableNotificationContent().apply { 10 | setTitle("Hello") 11 | setBody("World") 12 | setSound(UNNotificationSound.defaultSound()) 13 | } 14 | notif.addNotificationRequest( 15 | UNNotificationRequest.requestWithIdentifier( 16 | identifier = "hello", 17 | content = content, 18 | trigger = null 19 | ), 20 | null 21 | ) 22 | } 23 | } 24 | } 25 | ) 26 | } -------------------------------------------------------------------------------- /calf-permissions/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | id("module.publication") 4 | } 5 | 6 | kotlin { 7 | sourceSets.commonMain.dependencies { 8 | implementation(compose.runtime) 9 | implementation(compose.foundation) 10 | implementation(compose.material) 11 | } 12 | 13 | sourceSets.androidMain.dependencies { 14 | implementation(libs.activity.compose) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /calf-permissions/src/commonMain/kotlin/com.mohamedrejeb.calf/permissions/MutableMultiplePermissionsState.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | 6 | /** 7 | * Creates a [MultiplePermissionsState] that is remembered across compositions. 8 | * 9 | * It's recommended that apps exercise the permissions workflow as described in the 10 | * [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions). 11 | * 12 | * @param permissions the permissions to control and observe. 13 | * @param onPermissionsResult will be called with whether or not the user granted the permissions 14 | * after [MultiplePermissionsState.launchMultiplePermissionRequest] is called. 15 | */ 16 | @ExperimentalPermissionsApi 17 | @Composable 18 | internal expect fun rememberMutableMultiplePermissionsState( 19 | permissions: List, 20 | onPermissionsResult: (Map) -> Unit = {} 21 | ): MultiplePermissionsState 22 | 23 | /** 24 | * A state object that can be hoisted to control and observe multiple permission status changes. 25 | * 26 | * In most cases, this will be created via [rememberMutableMultiplePermissionsState]. 27 | * 28 | * @param mutablePermissions list of mutable permissions to control and observe. 29 | */ 30 | @ExperimentalPermissionsApi 31 | @Stable 32 | internal expect class MutableMultiplePermissionsState( 33 | mutablePermissions: List 34 | ) : MultiplePermissionsState { 35 | override val permissions: List 36 | 37 | override val revokedPermissions: List 38 | 39 | override val allPermissionsGranted: Boolean 40 | 41 | override val shouldShowRationale: Boolean 42 | 43 | override fun launchMultiplePermissionRequest() 44 | 45 | internal fun updatePermissionsStatus(permissionsStatus: Map) 46 | } -------------------------------------------------------------------------------- /calf-permissions/src/commonMain/kotlin/com.mohamedrejeb.calf/permissions/MutablePermissionState.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | 6 | /** 7 | * Creates a [MutablePermissionState] that is remembered across compositions. 8 | * 9 | * It's recommended that apps exercise the permissions workflow as described in the 10 | * [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions). 11 | * 12 | * @param permission the permission to control and observe. 13 | * @param onPermissionResult will be called with whether or not the user granted the permission 14 | * after [PermissionState.launchPermissionRequest] is called. 15 | */ 16 | @ExperimentalPermissionsApi 17 | @Composable 18 | internal expect fun rememberMutablePermissionState( 19 | permission: Permission, 20 | onPermissionResult: (Boolean) -> Unit = {} 21 | ): MutablePermissionState 22 | 23 | /** 24 | * A mutable state object that can be used to control and observe permission status changes. 25 | * 26 | * In most cases, this will be created via [rememberMutablePermissionState]. 27 | * 28 | * @param permission the permission to control and observe. 29 | * @param context to check the status of the [permission]. 30 | * @param activity to check if the user should be presented with a rationale for [permission]. 31 | */ 32 | @ExperimentalPermissionsApi 33 | @Stable 34 | internal interface MutablePermissionState: PermissionState { 35 | fun refreshPermissionStatus() 36 | } -------------------------------------------------------------------------------- /calf-permissions/src/commonMain/kotlin/com.mohamedrejeb.calf/permissions/PermissionsUtil.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions 2 | 3 | import androidx.compose.runtime.Stable 4 | 5 | @RequiresOptIn(message = "Calf Permissions is experimental. The API may be changed in the future.") 6 | @Retention(AnnotationRetention.BINARY) 7 | annotation class ExperimentalPermissionsApi 8 | 9 | /** 10 | * Model of the status of a permission. It can be granted or denied. 11 | * If denied, the user might need to be presented with a rationale. 12 | */ 13 | @ExperimentalPermissionsApi 14 | @Stable 15 | sealed interface PermissionStatus { 16 | data object Granted : PermissionStatus 17 | data class Denied( 18 | val shouldShowRationale: Boolean, 19 | ) : PermissionStatus 20 | } 21 | 22 | /** 23 | * `true` if the permission is granted. 24 | */ 25 | @ExperimentalPermissionsApi 26 | val PermissionStatus.isGranted: Boolean 27 | get() = this == PermissionStatus.Granted 28 | 29 | /** 30 | * `true` if the permission is not granted. 31 | */ 32 | @ExperimentalPermissionsApi 33 | val PermissionStatus.isNotGranted: Boolean 34 | get() = !isGranted 35 | 36 | /** 37 | * `true` if the permission is denied. 38 | */ 39 | @ExperimentalPermissionsApi 40 | val PermissionStatus.isDenied: Boolean 41 | get() = this is PermissionStatus.Denied 42 | 43 | /** 44 | * `true` if a rationale should be presented to the user. 45 | */ 46 | @ExperimentalPermissionsApi 47 | val PermissionStatus.shouldShowRationale: Boolean 48 | get() = when (this) { 49 | is PermissionStatus.Granted -> 50 | false 51 | 52 | is PermissionStatus.Denied -> 53 | shouldShowRationale 54 | } -------------------------------------------------------------------------------- /calf-permissions/src/desktopMain/kotlin/com/mohamedrejeb/calf/permissions/MutablePermissionState.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | 10 | /** 11 | * Creates a [MutablePermissionState] that is remembered across compositions. 12 | * 13 | * It's recommended that apps exercise the permissions workflow as described in the 14 | * [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions). 15 | * 16 | * @param permission the permission to control and observe. 17 | * @param onPermissionResult will be called with whether or not the user granted the permission 18 | * after [PermissionState.launchPermissionRequest] is called. 19 | */ 20 | @ExperimentalPermissionsApi 21 | @Composable 22 | internal actual fun rememberMutablePermissionState( 23 | permission: Permission, 24 | onPermissionResult: (Boolean) -> Unit 25 | ): MutablePermissionState { 26 | return remember(permission) { 27 | MutablePermissionStateImpl(permission) 28 | } 29 | } 30 | 31 | /** 32 | * A mutable state object that can be used to control and observe permission status changes. 33 | * 34 | * In most cases, this will be created via [rememberMutablePermissionState]. 35 | * 36 | * @param permission the permission to control and observe. 37 | */ 38 | @ExperimentalPermissionsApi 39 | @Stable 40 | internal class MutablePermissionStateImpl( 41 | override val permission: Permission, 42 | ) : MutablePermissionState { 43 | 44 | override var status: PermissionStatus by mutableStateOf(getPermissionStatus()) 45 | 46 | override fun launchPermissionRequest() {} 47 | 48 | override fun openAppSettings() {} 49 | 50 | override fun refreshPermissionStatus() {} 51 | 52 | private fun getPermissionStatus(): PermissionStatus { 53 | return PermissionStatus.Denied(false) 54 | } 55 | } -------------------------------------------------------------------------------- /calf-permissions/src/iosMain/kotlin/com.mohamedrejeb.calf/permissions/PermissionsUtil.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions 2 | 3 | import com.mohamedrejeb.calf.permissions.helper.AVCapturePermissionHelper 4 | import com.mohamedrejeb.calf.permissions.helper.BluetoothPermissionHelper 5 | import com.mohamedrejeb.calf.permissions.helper.CalendarPermissionHelper 6 | import com.mohamedrejeb.calf.permissions.helper.ContactPermissionHelper 7 | import com.mohamedrejeb.calf.permissions.helper.GalleryPermissionHelper 8 | import com.mohamedrejeb.calf.permissions.helper.GrantedPermissionHelper 9 | import com.mohamedrejeb.calf.permissions.helper.LocalNotificationPermissionHelper 10 | import com.mohamedrejeb.calf.permissions.helper.LocationPermissionHelper 11 | import com.mohamedrejeb.calf.permissions.helper.PermissionHelper 12 | import com.mohamedrejeb.calf.permissions.helper.RemoteNotificationPermissionHelper 13 | import platform.AVFoundation.AVMediaTypeAudio 14 | import platform.AVFoundation.AVMediaTypeVideo 15 | 16 | internal fun Permission.getPermissionDelegate(): PermissionHelper { 17 | return when (this) { 18 | Permission.Camera -> 19 | AVCapturePermissionHelper(AVMediaTypeVideo) 20 | 21 | Permission.Gallery, 22 | Permission.ReadImage, 23 | Permission.ReadVideo, 24 | -> 25 | GalleryPermissionHelper() 26 | 27 | Permission.ReadStorage, 28 | Permission.WriteStorage, 29 | Permission.ReadAudio, 30 | Permission.Call, 31 | -> 32 | GrantedPermissionHelper() 33 | 34 | Permission.FineLocation, 35 | Permission.CoarseLocation, 36 | Permission.BackgroundLocation, 37 | -> 38 | LocationPermissionHelper() 39 | 40 | Permission.Notification -> 41 | LocalNotificationPermissionHelper() 42 | 43 | Permission.RemoteNotification -> 44 | RemoteNotificationPermissionHelper() 45 | 46 | Permission.RecordAudio -> 47 | AVCapturePermissionHelper(AVMediaTypeAudio) 48 | 49 | Permission.BluetoothLe, 50 | Permission.BluetoothScan, 51 | Permission.BluetoothConnect, 52 | Permission.BluetoothAdvertise, 53 | -> 54 | BluetoothPermissionHelper() 55 | 56 | Permission.ReadContacts -> ContactPermissionHelper() 57 | Permission.ReadCalendar -> CalendarPermissionHelper() 58 | Permission.WriteCalendar -> CalendarPermissionHelper() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /calf-permissions/src/iosMain/kotlin/com.mohamedrejeb.calf/permissions/helper/AVCapturePermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions.helper 2 | 3 | import com.mohamedrejeb.calf.permissions.ExperimentalPermissionsApi 4 | import com.mohamedrejeb.calf.permissions.PermissionStatus 5 | import platform.AVFoundation.AVAuthorizationStatus 6 | import platform.AVFoundation.AVAuthorizationStatusAuthorized 7 | import platform.AVFoundation.AVAuthorizationStatusDenied 8 | import platform.AVFoundation.AVAuthorizationStatusNotDetermined 9 | import platform.AVFoundation.AVCaptureDevice 10 | import platform.AVFoundation.AVMediaType 11 | import platform.AVFoundation.authorizationStatusForMediaType 12 | import platform.AVFoundation.requestAccessForMediaType 13 | 14 | internal class AVCapturePermissionHelper( 15 | private val type: AVMediaType, 16 | ) : PermissionHelper { 17 | @OptIn(ExperimentalPermissionsApi::class) 18 | override fun launchPermissionRequest(onPermissionResult: (Boolean) -> Unit) { 19 | handleLaunchPermissionRequest( 20 | onPermissionResult = onPermissionResult, 21 | launchPermissionRequest = { 22 | AVCaptureDevice.requestAccessForMediaType(type) { 23 | onPermissionResult(it) 24 | } 25 | } 26 | ) 27 | } 28 | 29 | @OptIn(ExperimentalPermissionsApi::class) 30 | override fun getPermissionStatus(onPermissionResult: (PermissionStatus) -> Unit) { 31 | val status = getCurrentAuthorizationStatus() 32 | val permissionStatus = when (status) { 33 | AVAuthorizationStatusAuthorized -> 34 | PermissionStatus.Granted 35 | 36 | AVAuthorizationStatusNotDetermined -> 37 | PermissionStatus.Denied(shouldShowRationale = false) 38 | 39 | AVAuthorizationStatusDenied -> 40 | PermissionStatus.Denied(shouldShowRationale = true) 41 | 42 | else -> 43 | PermissionStatus.Denied(shouldShowRationale = true) 44 | } 45 | 46 | onPermissionResult(permissionStatus) 47 | } 48 | 49 | private fun getCurrentAuthorizationStatus(): AVAuthorizationStatus { 50 | return AVCaptureDevice.authorizationStatusForMediaType(type) 51 | } 52 | } -------------------------------------------------------------------------------- /calf-permissions/src/iosMain/kotlin/com.mohamedrejeb.calf/permissions/helper/CalendarPermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions.helper 2 | 3 | import com.mohamedrejeb.calf.permissions.ExperimentalPermissionsApi 4 | import com.mohamedrejeb.calf.permissions.PermissionStatus 5 | import platform.EventKit.EKEventStore 6 | 7 | internal class CalendarPermissionHelper : PermissionHelper { 8 | override fun launchPermissionRequest(onPermissionResult: (Boolean) -> Unit) { 9 | handleLaunchPermissionRequest( 10 | onPermissionResult = onPermissionResult, 11 | launchPermissionRequest = { 12 | EKEventStore().requestFullAccessToEventsWithCompletion { isOk, error -> 13 | if (isOk && error == null) 14 | onPermissionResult(true) 15 | else 16 | onPermissionResult(false) 17 | } 18 | } 19 | ) 20 | } 21 | 22 | @ExperimentalPermissionsApi 23 | override fun getPermissionStatus(onPermissionResult: (PermissionStatus) -> Unit) { 24 | onPermissionResult(PermissionStatus.Granted) 25 | } 26 | } -------------------------------------------------------------------------------- /calf-permissions/src/iosMain/kotlin/com.mohamedrejeb.calf/permissions/helper/ContactPermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions.helper 2 | 3 | import com.mohamedrejeb.calf.permissions.ExperimentalPermissionsApi 4 | import com.mohamedrejeb.calf.permissions.PermissionStatus 5 | import platform.Contacts.CNAuthorizationStatus 6 | import platform.Contacts.CNAuthorizationStatusAuthorized 7 | import platform.Contacts.CNAuthorizationStatusDenied 8 | import platform.Contacts.CNAuthorizationStatusNotDetermined 9 | import platform.Contacts.CNContactStore 10 | import platform.Contacts.CNEntityType 11 | 12 | internal class ContactPermissionHelper : PermissionHelper { 13 | override fun launchPermissionRequest(onPermissionResult: (Boolean) -> Unit) { 14 | handleLaunchPermissionRequest( 15 | onPermissionResult = onPermissionResult, 16 | launchPermissionRequest = { 17 | CNContactStore().requestAccessForEntityType(CNEntityType.CNEntityTypeContacts) { granted, _ -> 18 | onPermissionResult(granted) 19 | } 20 | } 21 | ) 22 | } 23 | 24 | @OptIn(ExperimentalPermissionsApi::class) 25 | override fun getPermissionStatus(onPermissionResult: (PermissionStatus) -> Unit) { 26 | val permissionStatus = when (getCurrentAuthorizationStatus()) { 27 | CNAuthorizationStatusAuthorized -> PermissionStatus.Granted 28 | 29 | CNAuthorizationStatusNotDetermined -> 30 | PermissionStatus.Denied(shouldShowRationale = false) 31 | 32 | CNAuthorizationStatusDenied -> 33 | PermissionStatus.Denied(shouldShowRationale = true) 34 | 35 | else -> PermissionStatus.Denied(shouldShowRationale = true) 36 | } 37 | onPermissionResult(permissionStatus) 38 | } 39 | 40 | private fun getCurrentAuthorizationStatus(): CNAuthorizationStatus { 41 | return CNContactStore.authorizationStatusForEntityType(CNEntityType.CNEntityTypeContacts) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /calf-permissions/src/iosMain/kotlin/com.mohamedrejeb.calf/permissions/helper/GalleryPermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions.helper 2 | 3 | import com.mohamedrejeb.calf.permissions.ExperimentalPermissionsApi 4 | import com.mohamedrejeb.calf.permissions.PermissionStatus 5 | import platform.Photos.PHAuthorizationStatus 6 | import platform.Photos.PHAuthorizationStatusAuthorized 7 | import platform.Photos.PHAuthorizationStatusDenied 8 | import platform.Photos.PHAuthorizationStatusNotDetermined 9 | import platform.Photos.PHPhotoLibrary 10 | 11 | internal class GalleryPermissionHelper : PermissionHelper { 12 | @OptIn(ExperimentalPermissionsApi::class) 13 | override fun launchPermissionRequest(onPermissionResult: (Boolean) -> Unit) { 14 | handleLaunchPermissionRequest( 15 | onPermissionResult = onPermissionResult, 16 | launchPermissionRequest = { 17 | PHPhotoLibrary.requestAuthorization { 18 | onPermissionResult(it == PHAuthorizationStatusAuthorized) 19 | } 20 | } 21 | ) 22 | } 23 | 24 | @OptIn(ExperimentalPermissionsApi::class) 25 | override fun getPermissionStatus(onPermissionResult: (PermissionStatus) -> Unit) { 26 | val status = getCurrentAuthorizationStatus() 27 | val permissionStatus = when (status) { 28 | PHAuthorizationStatusAuthorized -> 29 | PermissionStatus.Granted 30 | 31 | PHAuthorizationStatusNotDetermined -> 32 | PermissionStatus.Denied(shouldShowRationale = false) 33 | 34 | PHAuthorizationStatusDenied -> 35 | PermissionStatus.Denied(shouldShowRationale = true) 36 | 37 | else -> 38 | PermissionStatus.Denied(shouldShowRationale = true) 39 | } 40 | onPermissionResult(permissionStatus) 41 | } 42 | 43 | private fun getCurrentAuthorizationStatus(): PHAuthorizationStatus { 44 | return PHPhotoLibrary.authorizationStatus() 45 | } 46 | } -------------------------------------------------------------------------------- /calf-permissions/src/iosMain/kotlin/com.mohamedrejeb.calf/permissions/helper/GrantedPermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions.helper 2 | 3 | import com.mohamedrejeb.calf.permissions.ExperimentalPermissionsApi 4 | import com.mohamedrejeb.calf.permissions.PermissionStatus 5 | 6 | internal class GrantedPermissionHelper: PermissionHelper { 7 | override fun launchPermissionRequest(onPermissionResult: (Boolean) -> Unit) { 8 | onPermissionResult(true) 9 | } 10 | 11 | @ExperimentalPermissionsApi 12 | override fun getPermissionStatus(onPermissionResult: (PermissionStatus) -> Unit) = 13 | onPermissionResult(PermissionStatus.Granted) 14 | } -------------------------------------------------------------------------------- /calf-permissions/src/iosMain/kotlin/com.mohamedrejeb.calf/permissions/helper/PermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions.helper 2 | 3 | import com.mohamedrejeb.calf.permissions.ExperimentalPermissionsApi 4 | import com.mohamedrejeb.calf.permissions.PermissionStatus 5 | 6 | internal interface PermissionHelper { 7 | fun launchPermissionRequest(onPermissionResult: (Boolean) -> Unit) 8 | 9 | @OptIn(ExperimentalPermissionsApi::class) 10 | fun getPermissionStatus( 11 | onPermissionResult: (PermissionStatus) -> Unit 12 | ) 13 | } 14 | 15 | @OptIn(ExperimentalPermissionsApi::class) 16 | internal fun PermissionHelper.handleLaunchPermissionRequest( 17 | onPermissionResult: (Boolean) -> Unit, 18 | launchPermissionRequest: () -> Unit, 19 | ) { 20 | getPermissionStatus { status -> 21 | when (status) { 22 | is PermissionStatus.Granted -> 23 | onPermissionResult(true) 24 | 25 | is PermissionStatus.Denied -> 26 | if (status.shouldShowRationale) 27 | onPermissionResult(false) 28 | else 29 | launchPermissionRequest() 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /calf-permissions/src/jsMain/kotlin/com.mohamedrejeb.calf/permissions/MutablePermissionState.js.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | 10 | /** 11 | * Creates a [MutablePermissionState] that is remembered across compositions. 12 | * 13 | * It's recommended that apps exercise the permissions workflow as described in the 14 | * [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions). 15 | * 16 | * @param permission the permission to control and observe. 17 | * @param onPermissionResult will be called with whether or not the user granted the permission 18 | * after [PermissionState.launchPermissionRequest] is called. 19 | */ 20 | @ExperimentalPermissionsApi 21 | @Composable 22 | internal actual fun rememberMutablePermissionState( 23 | permission: Permission, 24 | onPermissionResult: (Boolean) -> Unit, 25 | ): MutablePermissionState { 26 | return remember(permission) { 27 | MutablePermissionStateImpl(permission) 28 | } 29 | } 30 | 31 | /** 32 | * A mutable state object that can be used to control and observe permission status changes. 33 | * 34 | * In most cases, this will be created via [rememberMutablePermissionState]. 35 | * 36 | * @param permission the permission to control and observe. 37 | */ 38 | @ExperimentalPermissionsApi 39 | @Stable 40 | internal class MutablePermissionStateImpl( 41 | override val permission: Permission, 42 | ) : MutablePermissionState { 43 | override var status: PermissionStatus by mutableStateOf(getPermissionStatus()) 44 | 45 | override fun launchPermissionRequest() {} 46 | 47 | override fun openAppSettings() {} 48 | 49 | override fun refreshPermissionStatus() {} 50 | 51 | private fun getPermissionStatus(): PermissionStatus { 52 | return PermissionStatus.Denied(false) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /calf-permissions/src/wasmJsMain/kotlin/com.mohamedrejeb.calf/permissions/MutablePermissionState.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.permissions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | 10 | /** 11 | * Creates a [MutablePermissionState] that is remembered across compositions. 12 | * 13 | * It's recommended that apps exercise the permissions workflow as described in the 14 | * [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions). 15 | * 16 | * @param permission the permission to control and observe. 17 | * @param onPermissionResult will be called with whether or not the user granted the permission 18 | * after [PermissionState.launchPermissionRequest] is called. 19 | */ 20 | @ExperimentalPermissionsApi 21 | @Composable 22 | internal actual fun rememberMutablePermissionState( 23 | permission: Permission, 24 | onPermissionResult: (Boolean) -> Unit, 25 | ): MutablePermissionState { 26 | return remember(permission) { 27 | MutablePermissionStateImpl(permission) 28 | } 29 | } 30 | 31 | /** 32 | * A mutable state object that can be used to control and observe permission status changes. 33 | * 34 | * In most cases, this will be created via [rememberMutablePermissionState]. 35 | * 36 | * @param permission the permission to control and observe. 37 | */ 38 | @ExperimentalPermissionsApi 39 | @Stable 40 | internal class MutablePermissionStateImpl( 41 | override val permission: Permission, 42 | ) : MutablePermissionState { 43 | override var status: PermissionStatus by mutableStateOf(getPermissionStatus()) 44 | 45 | override fun launchPermissionRequest() {} 46 | 47 | override fun openAppSettings() {} 48 | 49 | override fun refreshPermissionStatus() {} 50 | 51 | private fun getPermissionStatus(): PermissionStatus { 52 | return PermissionStatus.Denied(false) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /calf-sf-symbols/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | } 4 | 5 | kotlin { 6 | sourceSets.commonMain.dependencies { 7 | implementation(compose.runtime) 8 | implementation(compose.foundation) 9 | implementation(compose.material) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /calf-sf-symbols/src/commonMain/kotlin/com.mohamedrejeb.calf.sf.symbols/SFSymbols.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sf.symbols 2 | 3 | object SFSymbols 4 | -------------------------------------------------------------------------------- /calf-ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | id("module.publication") 4 | } 5 | 6 | kotlin { 7 | sourceSets.commonMain.dependencies { 8 | implementation(projects.calfCore) 9 | 10 | implementation(compose.runtime) 11 | implementation(compose.foundation) 12 | implementation(compose.material3) 13 | implementation(libs.kotlinx.coroutines.core) 14 | } 15 | 16 | sourceSets.androidMain.dependencies { 17 | implementation(libs.activity.compose) 18 | implementation(libs.kotlinx.coroutines.android) 19 | } 20 | 21 | sourceSets.desktopMain.dependencies { 22 | implementation(libs.kotlinx.coroutines.swing) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /calf-ui/src/androidMain/kotlin/com/mohamedrejeb/calf/ui/datepicker/AdaptiveDatePickerState.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.datepicker 2 | 3 | import androidx.compose.material3.CalendarLocale 4 | 5 | internal actual fun getCalendarLocalDefault(): CalendarLocale = 6 | CalendarLocale.getDefault() 7 | -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/ExperimentalCalfUiApi.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui 2 | 3 | @RequiresOptIn( 4 | "This Calf UI API is experimental and is likely to change or to be removed in" + 5 | " the future.", 6 | level = RequiresOptIn.Level.WARNING 7 | ) 8 | @Target( 9 | AnnotationTarget.CLASS, 10 | AnnotationTarget.FUNCTION, 11 | AnnotationTarget.PROPERTY 12 | ) 13 | @Retention(AnnotationRetention.BINARY) 14 | annotation class ExperimentalCalfUiApi() 15 | -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/datepicker/AdaptiveDatePicker.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.datepicker 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.material3.DatePickerFormatter 5 | import androidx.compose.material3.DatePickerColors 6 | import androidx.compose.material3.DatePickerDefaults 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | 13 | @OptIn(ExperimentalMaterial3Api::class) 14 | @Composable 15 | expect fun AdaptiveDatePicker( 16 | state: AdaptiveDatePickerState, 17 | modifier: Modifier = Modifier, 18 | dateFormatter: DatePickerFormatter = remember { DatePickerDefaults.dateFormatter() }, 19 | title: (@Composable () -> Unit)? = null, 20 | headline: (@Composable () -> Unit)? = null, 21 | showModeToggle: Boolean = true, 22 | colors: DatePickerColors = DatePickerDefaults.colors(), 23 | ) 24 | 25 | internal val DatePickerTitlePadding = PaddingValues(start = 24.dp, end = 12.dp, top = 16.dp) 26 | internal val DatePickerHeadlinePadding = PaddingValues(start = 24.dp, end = 12.dp, bottom = 12.dp) -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/datepicker/UIKitDisplayMode.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.datepicker 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlin.jvm.JvmInline 5 | 6 | @JvmInline 7 | @Immutable 8 | value class UIKitDisplayMode internal constructor(internal val value: Int) { 9 | companion object { 10 | /** Date picker mode */ 11 | val Picker = UIKitDisplayMode(0) 12 | 13 | /** Date text input mode */ 14 | val Wheels = UIKitDisplayMode(1) 15 | } 16 | 17 | override fun toString() = when (this) { 18 | Picker -> "Picker" 19 | Wheels -> "Wheels" 20 | else -> "Unknown" 21 | } 22 | } -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/dialog/uikit/AlertDialogIosAction.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.dialog.uikit 2 | 3 | /** 4 | * Represents an action in an iOS alert dialog. 5 | * 6 | * @param title The title of the action. 7 | * @param style The style of the action. 8 | * @param onClick The lambda that is invoked when the action is clicked. 9 | * @param isPreferred Indicates whether the action is the preferred action or not. 10 | * @param enabled Indicates whether the action is enabled or not. 11 | * 12 | * 13 | * See [UIAlertAction](https://developer.apple.com/documentation/uikit/uialertaction?language=objc) 14 | */ 15 | data class AlertDialogIosAction( 16 | val title: String, 17 | val style: AlertDialogIosActionStyle, 18 | val onClick: () -> Unit, 19 | val isPreferred: Boolean = false, 20 | val enabled: Boolean = true, 21 | ) -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/dialog/uikit/AlertDialogIosActionStyle.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.dialog.uikit 2 | 3 | /** 4 | * Represents the style of an action in an iOS alert dialog. 5 | * 6 | * See [UIAlertActionStyle](https://developer.apple.com/documentation/uikit/uialertaction/style-swift.property?language=objc) 7 | */ 8 | enum class AlertDialogIosActionStyle { 9 | /** 10 | * Apply the default style to the action’s button. 11 | */ 12 | Default, 13 | /** 14 | * Apply a style that indicates the action cancels the operation and leaves things unchanged. 15 | */ 16 | Cancel, 17 | /** 18 | * Apply a style that indicates the action might change or delete data. 19 | */ 20 | Destructive 21 | } -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/dialog/uikit/AlertDialogIosSeverity.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.dialog.uikit 2 | 3 | /** 4 | * This enumeration defines the severity options used by the severity property of `UIAlertController`. In apps built with Mac Catalyst, the severity determines the style of the presented alert. A `UIAlertControllerSeverityCritical` alert appears with a caution icon, and an alert with a `UIAlertControllerSeverityDefault` severity doesn’t. UIKit ignores the alert severity on iOS. 5 | * 6 | * You should only use the UIAlertControllerSeverityCritical severity if an alert truly requires special attention from the user. For more information, see the [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/macos/windows-and-views/alerts/) on alerts 7 | * 8 | * See [UIAlertControllerSeverity](https://developer.apple.com/documentation/uikit/uialertcontrollerseverity?language=objc) 9 | */ 10 | enum class AlertDialogIosSeverity { 11 | /** 12 | * Indicates that the system should present the alert using the standard alert style. 13 | */ 14 | Default, 15 | /** 16 | * Indicates that the system should present the alert using the critical, or caution, style. 17 | */ 18 | Critical 19 | } -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/dialog/uikit/AlertDialogIosStyle.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.dialog.uikit 2 | 3 | /** 4 | * Represents the style of an iOS alert dialog. 5 | * 6 | * See [UIAlertControllerStyle](https://developer.apple.com/documentation/uikit/uialertcontroller/style?language=objc) 7 | */ 8 | enum class AlertDialogIosStyle { 9 | /** 10 | * An alert displayed modally for the app. 11 | */ 12 | Alert, 13 | 14 | /** 15 | * An action sheet displayed by the view controller that presented it. 16 | */ 17 | ActionSheet 18 | } -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/dialog/uikit/AlertDialogIosTextField.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.dialog.uikit 2 | 3 | import com.mohamedrejeb.calf.ui.uikit.IosKeyboardType 4 | 5 | /** 6 | * Represents a text field in an iOS alert dialog. 7 | * 8 | * @param placeholder The placeholder of the text field. 9 | * @param initialValue The initial value of the text field. 10 | * @param keyboardType The keyboard type of the text field. 11 | * @param isSecure Indicates whether the text field is secure or not. 12 | * @param onValueChange The lambda that is invoked when the text field value changes. 13 | * 14 | * 15 | * See [UITextField](https://developer.apple.com/documentation/uikit/uitextfield?language=objc) 16 | */ 17 | data class AlertDialogIosTextField( 18 | val placeholder: String = "", 19 | val initialValue: String = "", 20 | val keyboardType: IosKeyboardType = IosKeyboardType.Default, 21 | val isSecure: Boolean = false, 22 | val onValueChange: (String) -> Unit, 23 | ) -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/dropdown/AdaptiveDropDownMenu.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.dropdown 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | 7 | @Composable 8 | internal expect fun BoxScope.AdaptiveDropdownMenu( 9 | expanded: Boolean, 10 | onDismissRequest: () -> Unit, 11 | modifier: Modifier = Modifier, 12 | ) 13 | -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/gesture/AdaptiveClickable.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.gesture 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.LocalIndication 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.RectangleShape 10 | import androidx.compose.ui.graphics.Shape 11 | import androidx.compose.ui.semantics.Role 12 | 13 | @Composable 14 | expect fun Modifier.adaptiveClickable( 15 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 16 | indication: Indication? = LocalIndication.current, 17 | enabled: Boolean = true, 18 | onClickLabel: String? = null, 19 | role: Role? = null, 20 | shape: Shape = RectangleShape, 21 | onClick: () -> Unit, 22 | ): Modifier -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/progress/AdaptiveCircularProgressIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.progress 2 | 3 | import androidx.compose.material3.ProgressIndicatorDefaults 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.graphics.StrokeCap 8 | import androidx.compose.ui.unit.Dp 9 | 10 | @Composable 11 | expect fun AdaptiveCircularProgressIndicator( 12 | modifier: Modifier = Modifier, 13 | color: Color = ProgressIndicatorDefaults.circularColor, 14 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, 15 | trackColor: Color = ProgressIndicatorDefaults.circularTrackColor, 16 | strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularIndeterminateStrokeCap, 17 | ) -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/sheet/AdaptiveBottomSheet.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.sheet 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.WindowInsets 5 | import androidx.compose.material3.BottomSheetDefaults 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.ModalBottomSheetDefaults 8 | import androidx.compose.material3.ModalBottomSheetProperties 9 | import androidx.compose.material3.contentColorFor 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.Shape 14 | import androidx.compose.ui.unit.Dp 15 | import androidx.compose.ui.unit.dp 16 | 17 | @OptIn(ExperimentalMaterial3Api::class) 18 | @Composable 19 | expect fun AdaptiveBottomSheet( 20 | onDismissRequest: () -> Unit, 21 | modifier: Modifier = Modifier, 22 | adaptiveSheetState: AdaptiveSheetState, 23 | sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, 24 | shape: Shape = BottomSheetDefaults.ExpandedShape, 25 | containerColor: Color = BottomSheetDefaults.ContainerColor, 26 | contentColor: Color = contentColorFor(containerColor), 27 | tonalElevation: Dp = 0.dp, 28 | scrimColor: Color = BottomSheetDefaults.ScrimColor, 29 | dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, 30 | contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, 31 | properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties, 32 | content: @Composable ColumnScope.() -> Unit, 33 | ) -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/timepicker/AdaptiveTimePicker.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.timepicker 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.TimePickerColors 5 | import androidx.compose.material3.TimePickerDefaults 6 | import androidx.compose.material3.TimePickerLayoutType 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | 10 | @OptIn(ExperimentalMaterial3Api::class) 11 | @Composable 12 | expect fun AdaptiveTimePicker( 13 | state: AdaptiveTimePickerState, 14 | modifier: Modifier = Modifier, 15 | colors: TimePickerColors = TimePickerDefaults.colors(), 16 | layoutType: TimePickerLayoutType = TimePickerDefaults.layoutType(), 17 | ) -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/timepicker/AdaptiveTimePickerState.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.timepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.runtime.saveable.Saver 7 | import androidx.compose.runtime.saveable.rememberSaveable 8 | 9 | @Composable 10 | fun rememberAdaptiveTimePickerState( 11 | initialHour: Int = 0, 12 | initialMinute: Int = 0, 13 | is24Hour: Boolean = false, 14 | ): AdaptiveTimePickerState = rememberSaveable( 15 | saver = AdaptiveTimePickerState.Saver() 16 | ) { 17 | AdaptiveTimePickerState( 18 | initialHour = initialHour, 19 | initialMinute = initialMinute, 20 | is24Hour = is24Hour, 21 | ) 22 | } 23 | 24 | /** 25 | * A class to handle state changes in a [TimePicker] 26 | * 27 | * @sample androidx.compose.material3.samples.TimePickerSample 28 | * 29 | * @param initialHour 30 | * starting hour for this state, will be displayed in the time picker when launched 31 | * Ranges from 0 to 23 32 | * @param initialMinute 33 | * starting minute for this state, will be displayed in the time picker when launched. 34 | * Ranges from 0 to 59 35 | * @param is24Hour The format for this time picker `false` for 12 hour format with an AM/PM toggle 36 | * or `true` for 24 hour format without toggle. 37 | */ 38 | @Stable 39 | expect class AdaptiveTimePickerState( 40 | initialHour: Int, 41 | initialMinute: Int, 42 | is24Hour: Boolean, 43 | ) { 44 | val minute: Int 45 | val hour: Int 46 | val is24hour: Boolean 47 | 48 | companion object { 49 | /** 50 | * The default [Saver] implementation for [TimePickerState]. 51 | */ 52 | fun Saver(): Saver 53 | } 54 | } -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/toggle/AdaptiveSwitch.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.toggle 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.material3.Switch 5 | import androidx.compose.material3.SwitchColors 6 | import androidx.compose.material3.SwitchDefaults 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Modifier 10 | 11 | @Composable 12 | expect fun AdaptiveSwitch( 13 | checked: Boolean, 14 | onCheckedChange: ((Boolean) -> Unit)?, 15 | modifier: Modifier = Modifier, 16 | thumbContent: (@Composable () -> Unit)? = null, 17 | enabled: Boolean = true, 18 | colors: SwitchColors = SwitchDefaults.colors(), 19 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 20 | ) -------------------------------------------------------------------------------- /calf-ui/src/commonMain/kotlin/com/mohamedrejeb/calf/ui/uikit/IosKeyboardType.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.uikit 2 | 3 | /** 4 | * Represents the type of the keyboard of a text field in an iOS alert dialog. 5 | * 6 | * See [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype?language=objc) 7 | */ 8 | enum class IosKeyboardType { 9 | /** 10 | * Specifies the default keyboard for the current input method. 11 | */ 12 | Default, 13 | 14 | /** 15 | * Specifies a keyboard that displays standard ASCII characters. 16 | */ 17 | AsciiCapable, 18 | 19 | /** 20 | * Specifies a keyboard that displays numbers and punctuation. 21 | */ 22 | NumbersAndPunctuation, 23 | 24 | /** 25 | * Specifies a keyboard for URL entry. 26 | */ 27 | URL, 28 | 29 | /** 30 | * Specifies a numeric keypad for PIN entry. 31 | */ 32 | NumberPad, 33 | 34 | /** 35 | * Specifies a keypad for entering telephone numbers. 36 | */ 37 | PhonePad, 38 | 39 | /** 40 | * Specifies a keypad for entering a person’s name or phone number. 41 | */ 42 | NamePhonePad, 43 | 44 | /** 45 | * Specifies a keyboard for entering email addresses. 46 | */ 47 | EmailAddress, 48 | 49 | /** 50 | * Specifies a keyboard with numbers and a decimal point. 51 | */ 52 | DecimalPad, 53 | 54 | /** 55 | * Specifies a keyboard for Twitter text entry, with easy access to the at (”@”) and hash (”#”) characters. 56 | */ 57 | Twitter, 58 | 59 | /** 60 | * Specifies a keyboard for web search terms and URL entry. 61 | */ 62 | WebSearch, 63 | 64 | /** 65 | * Specifies a number pad that outputs only ASCII digits. 66 | */ 67 | AsciiCapableNumberPad, 68 | ; 69 | } -------------------------------------------------------------------------------- /calf-ui/src/desktopMain/kotlin/com/mohamedrejeb/calf/ui/datepicker/AdaptiveDatePickerState.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.datepicker 2 | 3 | import androidx.compose.material3.CalendarLocale 4 | 5 | internal actual fun getCalendarLocalDefault(): CalendarLocale = 6 | CalendarLocale.getDefault() -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/gesture/AdaptiveClickable.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.gesture 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.foundation.Indication 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.interaction.MutableInteractionSource 7 | import androidx.compose.foundation.interaction.collectIsPressedAsState 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.graphics.Shape 13 | import androidx.compose.ui.graphics.graphicsLayer 14 | import androidx.compose.ui.semantics.Role 15 | 16 | @Composable 17 | actual fun Modifier.adaptiveClickable( 18 | interactionSource: MutableInteractionSource, 19 | indication: Indication?, 20 | enabled: Boolean, 21 | onClickLabel: String?, 22 | role: Role?, 23 | shape: Shape, 24 | onClick: () -> Unit 25 | ): Modifier { 26 | val isPressed by interactionSource.collectIsPressedAsState() 27 | val targetScale = if (isPressed) IOS_BUTTON_SCALE_WHEN_PRESSED else 1f 28 | val scale by animateFloatAsState(targetValue = targetScale) 29 | 30 | return this 31 | .graphicsLayer { 32 | scaleX = scale 33 | scaleY = scale 34 | } 35 | .clip(shape) 36 | .clickable( 37 | interactionSource = interactionSource, 38 | indication = null, 39 | enabled = enabled, 40 | onClickLabel = onClickLabel, 41 | role = role, 42 | onClick = onClick 43 | ) 44 | } 45 | 46 | private const val IOS_BUTTON_SCALE_WHEN_PRESSED = 0.96f 47 | -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/progress/AdaptiveCircularProgressIndicator.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.progress 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.StrokeCap 7 | import androidx.compose.ui.unit.Dp 8 | import com.mohamedrejeb.calf.ui.cupertino.CupertinoActivityIndicator 9 | 10 | @Composable 11 | actual fun AdaptiveCircularProgressIndicator( 12 | modifier: Modifier, 13 | color: Color, 14 | strokeWidth: Dp, 15 | trackColor: Color, 16 | strokeCap: StrokeCap, 17 | ) { 18 | CupertinoActivityIndicator( 19 | modifier = modifier, 20 | color = color, 21 | ) 22 | } -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/timepicker/AdaptiveTimePickerState.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.timepicker 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.TimePickerState 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.saveable.Saver 9 | import androidx.compose.runtime.setValue 10 | 11 | 12 | /** 13 | * A class to handle state changes in a [TimePicker] 14 | * 15 | * @sample androidx.compose.material3.samples.TimePickerSample 16 | * 17 | * @param initialHour 18 | * starting hour for this state, will be displayed in the time picker when launched 19 | * Ranges from 0 to 23 20 | * @param initialMinute 21 | * starting minute for this state, will be displayed in the time picker when launched. 22 | * Ranges from 0 to 59 23 | * @param is24Hour The format for this time picker `false` for 12 hour format with an AM/PM toggle 24 | * or `true` for 24 hour format without toggle. 25 | */ 26 | @Stable 27 | @OptIn(ExperimentalMaterial3Api::class) 28 | actual class AdaptiveTimePickerState actual constructor( 29 | initialHour: Int, 30 | initialMinute: Int, 31 | is24Hour: Boolean, 32 | ) { 33 | init { 34 | require(initialHour in 0..23) { "initialHour should in [0..23] range" } 35 | require(initialHour in 0..59) { "initialMinute should be in [0..59] range" } 36 | } 37 | 38 | internal var hourState by mutableStateOf(initialHour) 39 | internal var minuteState by mutableStateOf(initialMinute) 40 | internal var is24hourState by mutableStateOf(is24Hour) 41 | 42 | actual val hour: Int get() = hourState 43 | actual val minute: Int get() = minuteState 44 | actual val is24hour: Boolean get() = is24hourState 45 | 46 | actual companion object { 47 | /** 48 | * The default [Saver] implementation for [TimePickerState]. 49 | */ 50 | actual fun Saver(): Saver = Saver( 51 | save = { 52 | listOf( 53 | it.hour, 54 | it.minute, 55 | it.is24hour 56 | ) 57 | }, 58 | restore = { value -> 59 | AdaptiveTimePickerState( 60 | initialHour = value[0] as Int, 61 | initialMinute = value[1] as Int, 62 | is24Hour = value[2] as Boolean 63 | ) 64 | } 65 | ) 66 | } 67 | } -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/timepicker/TimePickerManager.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.timepicker 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mohamedrejeb.calf.core.InternalCalfApi 5 | import kotlinx.cinterop.BetaInteropApi 6 | import kotlinx.cinterop.ExperimentalForeignApi 7 | import kotlinx.cinterop.ObjCAction 8 | import kotlinx.cinterop.useContents 9 | import platform.Foundation.* 10 | import platform.UIKit.* 11 | import platform.darwin.NSObject 12 | import platform.objc.sel_registerName 13 | 14 | @OptIn(ExperimentalForeignApi::class) 15 | class TimePickerManager internal constructor( 16 | private val datePicker: UIDatePicker, 17 | initialMinute: Int, 18 | initialHour: Int, 19 | is24Hour: Boolean, 20 | private val onHourChanged: (hour: Int) -> Unit, 21 | private val onMinuteChanged: (minute: Int) -> Unit, 22 | ) { 23 | private val datePickerDelegate = object : NSObject() { 24 | @ObjCAction 25 | fun onTimeChanged(sender: UIDatePicker) { 26 | val components = NSCalendar.currentCalendar.components( 27 | NSCalendarUnitHour or NSCalendarUnitMinute, 28 | sender.date 29 | ) 30 | 31 | onHourChanged(components.hour.toInt()) 32 | onMinuteChanged(components.minute.toInt()) 33 | } 34 | } 35 | 36 | val datePickerWidth = mutableStateOf(0f) 37 | val datePickerHeight = mutableStateOf(0f) 38 | 39 | init { 40 | val dateComponents = NSDateComponents().apply { 41 | minute = initialMinute.toLong() 42 | hour = initialHour.toLong() 43 | } 44 | 45 | val date = NSCalendar.currentCalendar.dateFromComponents(dateComponents) ?: NSDate() 46 | 47 | datePicker.apply { 48 | this.date = date 49 | locale = NSLocale.currentLocale 50 | datePickerMode = UIDatePickerMode.UIDatePickerModeTime 51 | preferredDatePickerStyle = UIDatePickerStyle.UIDatePickerStyleWheels 52 | 53 | // Add target using NSObject delegate 54 | addTarget( 55 | target = datePickerDelegate, 56 | action = sel_registerName("onTimeChanged:"), 57 | forControlEvents = UIControlEventValueChanged 58 | ) 59 | } 60 | 61 | datePicker.frame.useContents { 62 | datePickerWidth.value = this.size.width.toFloat() 63 | datePickerHeight.value = this.size.height.toFloat() 64 | } 65 | } 66 | 67 | 68 | } -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/toggle/AdaptiveSwitch.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.toggle 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.material3.SwitchColors 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | 8 | @Composable 9 | actual fun AdaptiveSwitch( 10 | checked: Boolean, 11 | onCheckedChange: ((Boolean) -> Unit)?, 12 | modifier: Modifier, 13 | thumbContent: (@Composable () -> Unit)?, 14 | enabled: Boolean, 15 | colors: SwitchColors, 16 | interactionSource: MutableInteractionSource, 17 | ) { 18 | CupertinoSwitch( 19 | checked = checked, 20 | onCheckedChange = onCheckedChange, 21 | modifier = modifier, 22 | thumbContent = thumbContent, 23 | enabled = enabled, 24 | colors = colors, 25 | interactionSource = interactionSource, 26 | ) 27 | } -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/utils/ColorHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.utils 2 | 3 | import androidx.compose.material3.ColorScheme 4 | import androidx.compose.material3.LocalTonalElevationEnabled 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.surfaceColorAtElevation 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.ReadOnlyComposable 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.unit.Dp 11 | import platform.UIKit.UIColor 12 | 13 | fun Color.toUIColor(): UIColor { 14 | return UIColor.colorWithRed( 15 | red = this.red.toDouble(), 16 | green = this.green.toDouble(), 17 | blue = this.blue.toDouble(), 18 | alpha = this.alpha.toDouble(), 19 | ) 20 | } 21 | 22 | @Composable 23 | internal fun surfaceColorAtElevation(color: Color, elevation: Dp): Color = 24 | MaterialTheme.colorScheme.applyTonalElevation(color, elevation) 25 | 26 | @Composable 27 | @ReadOnlyComposable 28 | internal fun ColorScheme.applyTonalElevation(backgroundColor: Color, elevation: Dp): Color { 29 | val tonalElevationEnabled = LocalTonalElevationEnabled.current 30 | return if (backgroundColor == surface && tonalElevationEnabled) { 31 | surfaceColorAtElevation(elevation) 32 | } else { 33 | backgroundColor 34 | } 35 | } -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/utils/IosKeyboardTypeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.utils 2 | 3 | import com.mohamedrejeb.calf.ui.uikit.IosKeyboardType 4 | import platform.UIKit.UIKeyboardTypeASCIICapable 5 | import platform.UIKit.UIKeyboardTypeASCIICapableNumberPad 6 | import platform.UIKit.UIKeyboardTypeDecimalPad 7 | import platform.UIKit.UIKeyboardTypeDefault 8 | import platform.UIKit.UIKeyboardTypeEmailAddress 9 | import platform.UIKit.UIKeyboardTypeNamePhonePad 10 | import platform.UIKit.UIKeyboardTypeNumberPad 11 | import platform.UIKit.UIKeyboardTypeNumbersAndPunctuation 12 | import platform.UIKit.UIKeyboardTypePhonePad 13 | import platform.UIKit.UIKeyboardTypeTwitter 14 | import platform.UIKit.UIKeyboardTypeURL 15 | import platform.UIKit.UIKeyboardTypeWebSearch 16 | 17 | /** 18 | * Converts an [IosKeyboardType] to a [platform.UIKit.UIKeyboardType]. 19 | * 20 | * @return The [platform.UIKit.UIKeyboardType] equivalent of the [IosKeyboardType]. 21 | */ 22 | fun IosKeyboardType.toUIKeyboardType(): Long { 23 | return when (this) { 24 | IosKeyboardType.Default -> UIKeyboardTypeDefault 25 | IosKeyboardType.AsciiCapable -> UIKeyboardTypeASCIICapable 26 | IosKeyboardType.NumbersAndPunctuation -> UIKeyboardTypeNumbersAndPunctuation 27 | IosKeyboardType.URL -> UIKeyboardTypeURL 28 | IosKeyboardType.NumberPad -> UIKeyboardTypeNumberPad 29 | IosKeyboardType.PhonePad -> UIKeyboardTypePhonePad 30 | IosKeyboardType.NamePhonePad -> UIKeyboardTypeNamePhonePad 31 | IosKeyboardType.EmailAddress -> UIKeyboardTypeEmailAddress 32 | IosKeyboardType.DecimalPad -> UIKeyboardTypeDecimalPad 33 | IosKeyboardType.Twitter -> UIKeyboardTypeTwitter 34 | IosKeyboardType.WebSearch -> UIKeyboardTypeWebSearch 35 | IosKeyboardType.AsciiCapableNumberPad -> UIKeyboardTypeASCIICapableNumberPad 36 | } 37 | } -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/utils/ThemeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.utils 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import platform.UIKit.UIUserInterfaceStyle 5 | import platform.UIKit.UIView 6 | import platform.UIKit.UIViewController 7 | 8 | internal fun UIViewController.applyTheme(dark: Boolean) { 9 | overrideUserInterfaceStyle = 10 | if (dark) 11 | UIUserInterfaceStyle.UIUserInterfaceStyleDark 12 | else 13 | UIUserInterfaceStyle.UIUserInterfaceStyleLight 14 | } 15 | 16 | internal fun UIView.applyTheme(dark : Boolean){ 17 | listOf(this, superview).forEach { 18 | it?.overrideUserInterfaceStyle = 19 | if (dark) 20 | UIUserInterfaceStyle.UIUserInterfaceStyleDark 21 | else 22 | UIUserInterfaceStyle.UIUserInterfaceStyleLight 23 | } 24 | } 25 | 26 | internal fun isDark(color: Color): Boolean { 27 | val r = color.red * 255 28 | val g = color.green * 255 29 | val b = color.blue * 255 30 | 31 | val luminance = 0.299 * r + 0.587 * g + 0.114 * b 32 | return luminance <= 128 33 | } -------------------------------------------------------------------------------- /calf-ui/src/iosMain/kotlin/com/mohamedrejeb/calf/ui/utils/datetime/KotlinxDatetimeCalendarModel.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.utils.datetime 2 | 3 | import kotlinx.datetime.Instant 4 | import kotlinx.datetime.TimeZone 5 | import kotlinx.datetime.atStartOfDayIn 6 | import kotlinx.datetime.toLocalDateTime 7 | 8 | internal class KotlinxDatetimeCalendarModel { 9 | fun getCanonicalDate(timeInMillis: Long): CalendarDate { 10 | return Instant 11 | .fromEpochMilliseconds(timeInMillis) 12 | .toLocalDateTime(TimeZone.UTC) 13 | .date 14 | .atStartOfDayIn(TimeZone.UTC) 15 | .toCalendarDate(TimeZone.UTC) 16 | } 17 | } 18 | 19 | internal fun Instant.toCalendarDate( 20 | timeZone : TimeZone 21 | ) : CalendarDate { 22 | 23 | val dateTime = toLocalDateTime(timeZone) 24 | 25 | return CalendarDate( 26 | year = dateTime.year, 27 | month = dateTime.monthNumber, 28 | dayOfMonth = dateTime.dayOfMonth, 29 | utcTimeMillis = toEpochMilliseconds() 30 | ) 31 | } 32 | 33 | /** 34 | * Represents a calendar date. 35 | * 36 | * @param year the date's year 37 | * @param month the date's month 38 | * @param dayOfMonth the date's day of month 39 | * @param utcTimeMillis the date representation in _UTC_ milliseconds from the epoch 40 | */ 41 | internal data class CalendarDate( 42 | val year: Int, 43 | val month: Int, 44 | val dayOfMonth: Int, 45 | val utcTimeMillis: Long 46 | ) : Comparable { 47 | override operator fun compareTo(other: CalendarDate): Int = 48 | this.utcTimeMillis.compareTo(other.utcTimeMillis) 49 | } 50 | -------------------------------------------------------------------------------- /calf-ui/src/jsMain/kotlin/com/mohamedrejeb/calf/ui/datepicker/AdaptiveDatePickerState.js.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.datepicker 2 | 3 | import androidx.compose.material3.CalendarLocale 4 | 5 | internal actual fun getCalendarLocalDefault(): CalendarLocale = 6 | CalendarLocale.current -------------------------------------------------------------------------------- /calf-ui/src/materialMain/kotlin/com/mohamedrejeb/calf/ui/datepicker/AdaptiveDatePicker.material.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.datepicker 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | 8 | @OptIn(ExperimentalMaterial3Api::class) 9 | @Composable 10 | actual fun AdaptiveDatePicker( 11 | state: AdaptiveDatePickerState, 12 | modifier: Modifier, 13 | dateFormatter: DatePickerFormatter, 14 | title: @Composable() (() -> Unit)?, 15 | headline: @Composable() (() -> Unit)?, 16 | showModeToggle: Boolean, 17 | colors: DatePickerColors 18 | ) { 19 | DatePicker( 20 | state = state.datePickerState, 21 | modifier = modifier, 22 | dateFormatter = dateFormatter, 23 | title = title, 24 | headline = headline, 25 | showModeToggle = showModeToggle, 26 | colors = colors, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /calf-ui/src/materialMain/kotlin/com/mohamedrejeb/calf/ui/dropdown/AdaptiveDropDownMenu.material.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.dropdown 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.material3.DropdownMenu 5 | import androidx.compose.material3.DropdownMenuItem 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | actual fun BoxScope.AdaptiveDropdownMenu( 12 | expanded: Boolean, 13 | onDismissRequest: () -> Unit, 14 | modifier: Modifier, 15 | ) { 16 | DropdownMenu( 17 | expanded = expanded, 18 | onDismissRequest = onDismissRequest, 19 | modifier = modifier, 20 | ) { 21 | DropdownMenuItem( 22 | text = { Text("Load") }, 23 | onClick = { println("Load") } 24 | ) 25 | DropdownMenuItem( 26 | text = { Text("Save") }, 27 | onClick = { println("Save") } 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /calf-ui/src/materialMain/kotlin/com/mohamedrejeb/calf/ui/gesture/AdaptiveClickable.material.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.gesture 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.draw.clip 9 | import androidx.compose.ui.graphics.Shape 10 | import androidx.compose.ui.semantics.Role 11 | 12 | @Composable 13 | actual fun Modifier.adaptiveClickable( 14 | interactionSource: MutableInteractionSource, 15 | indication: Indication?, 16 | enabled: Boolean, 17 | onClickLabel: String?, 18 | role: Role?, 19 | shape: Shape, 20 | onClick: () -> Unit 21 | ): Modifier = 22 | this 23 | .clip(shape) 24 | .clickable( 25 | interactionSource = interactionSource, 26 | indication = indication, 27 | enabled = enabled, 28 | onClickLabel = onClickLabel, 29 | role = role, 30 | onClick = onClick 31 | ) -------------------------------------------------------------------------------- /calf-ui/src/materialMain/kotlin/com/mohamedrejeb/calf/ui/progress/AdaptiveCircularProgressIndicator.material.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.progress 2 | 3 | import androidx.compose.material3.CircularProgressIndicator 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.graphics.StrokeCap 8 | import androidx.compose.ui.unit.Dp 9 | 10 | @Composable 11 | actual fun AdaptiveCircularProgressIndicator( 12 | modifier: Modifier, 13 | color: Color, 14 | strokeWidth: Dp, 15 | trackColor: Color, 16 | strokeCap: StrokeCap, 17 | ) { 18 | CircularProgressIndicator( 19 | modifier = modifier, 20 | color = color, 21 | strokeWidth = strokeWidth, 22 | trackColor = trackColor, 23 | strokeCap = strokeCap, 24 | ) 25 | } -------------------------------------------------------------------------------- /calf-ui/src/materialMain/kotlin/com/mohamedrejeb/calf/ui/sheet/AdaptiveBottomSheet.material.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.sheet 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.WindowInsets 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.ModalBottomSheet 7 | import androidx.compose.material3.ModalBottomSheetProperties 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.Shape 12 | import androidx.compose.ui.unit.Dp 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | actual fun AdaptiveBottomSheet( 17 | onDismissRequest: () -> Unit, 18 | modifier: Modifier, 19 | adaptiveSheetState: AdaptiveSheetState, 20 | sheetMaxWidth: Dp, 21 | shape: Shape, 22 | containerColor: Color, 23 | contentColor: Color, 24 | tonalElevation: Dp, 25 | scrimColor: Color, 26 | dragHandle: @Composable (() -> Unit)?, 27 | contentWindowInsets: @Composable () -> WindowInsets, 28 | properties: ModalBottomSheetProperties, 29 | content: @Composable ColumnScope.() -> Unit, 30 | ) { 31 | ModalBottomSheet( 32 | onDismissRequest = onDismissRequest, 33 | modifier = modifier, 34 | sheetState = adaptiveSheetState.materialSheetState, 35 | sheetMaxWidth = sheetMaxWidth, 36 | shape = shape, 37 | containerColor = containerColor, 38 | contentColor = contentColor, 39 | tonalElevation = tonalElevation, 40 | scrimColor = scrimColor, 41 | dragHandle = dragHandle, 42 | contentWindowInsets = contentWindowInsets, 43 | properties = properties, 44 | content = content, 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /calf-ui/src/materialMain/kotlin/com/mohamedrejeb/calf/ui/timepicker/AdaptiveTimePicker.material.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.timepicker 2 | 3 | import androidx.compose.material3.* 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | 7 | @OptIn(ExperimentalMaterial3Api::class) 8 | @Composable 9 | actual fun AdaptiveTimePicker( 10 | state: AdaptiveTimePickerState, 11 | modifier: Modifier, 12 | colors: TimePickerColors, 13 | layoutType: TimePickerLayoutType, 14 | ) { 15 | TimePicker( 16 | state = state.timePickerState, 17 | modifier = modifier, 18 | colors = colors, 19 | layoutType = layoutType, 20 | ) 21 | } -------------------------------------------------------------------------------- /calf-ui/src/materialMain/kotlin/com/mohamedrejeb/calf/ui/timepicker/AdaptiveTimePickerState.material.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.timepicker 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.TimePickerState 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.runtime.saveable.Saver 7 | 8 | 9 | /** 10 | * A class to handle state changes in a [TimePicker] 11 | * 12 | * @sample androidx.compose.material3.samples.TimePickerSample 13 | * 14 | * @param initialHour 15 | * starting hour for this state, will be displayed in the time picker when launched 16 | * Ranges from 0 to 23 17 | * @param initialMinute 18 | * starting minute for this state, will be displayed in the time picker when launched. 19 | * Ranges from 0 to 59 20 | * @param is24Hour The format for this time picker `false` for 12 hour format with an AM/PM toggle 21 | * or `true` for 24 hour format without toggle. 22 | */ 23 | @Stable 24 | @OptIn(ExperimentalMaterial3Api::class) 25 | actual class AdaptiveTimePickerState actual constructor( 26 | initialHour: Int, 27 | initialMinute: Int, 28 | is24Hour: Boolean, 29 | ) { 30 | init { 31 | require(initialHour in 0..23) { "initialHour should in [0..23] range" } 32 | require(initialHour in 0..59) { "initialMinute should be in [0..59] range" } 33 | } 34 | 35 | val timePickerState = TimePickerState( 36 | initialHour = initialHour, 37 | initialMinute = initialMinute, 38 | is24Hour = is24Hour 39 | ) 40 | 41 | actual val minute: Int get() = timePickerState.minute 42 | actual val hour: Int get() = timePickerState.hour 43 | actual val is24hour: Boolean get() = timePickerState.is24hour 44 | 45 | actual companion object { 46 | /** 47 | * The default [Saver] implementation for [TimePickerState]. 48 | */ 49 | actual fun Saver(): Saver = Saver( 50 | save = { 51 | listOf( 52 | it.hour, 53 | it.minute, 54 | it.is24hour 55 | ) 56 | }, 57 | restore = { value -> 58 | AdaptiveTimePickerState( 59 | initialHour = value[0] as Int, 60 | initialMinute = value[1] as Int, 61 | is24Hour = value[2] as Boolean 62 | ) 63 | } 64 | ) 65 | } 66 | } -------------------------------------------------------------------------------- /calf-ui/src/materialMain/kotlin/com/mohamedrejeb/calf/ui/toggle/AdaptiveSwitch.material.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.toggle 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.material3.Switch 5 | import androidx.compose.material3.SwitchColors 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | 9 | @Composable 10 | actual fun AdaptiveSwitch( 11 | checked: Boolean, 12 | onCheckedChange: ((Boolean) -> Unit)?, 13 | modifier: Modifier, 14 | thumbContent: (@Composable () -> Unit)?, 15 | enabled: Boolean, 16 | colors: SwitchColors, 17 | interactionSource: MutableInteractionSource, 18 | ) { 19 | Switch( 20 | checked = checked, 21 | onCheckedChange = onCheckedChange, 22 | modifier = modifier, 23 | thumbContent = thumbContent, 24 | enabled = enabled, 25 | colors = colors, 26 | interactionSource = interactionSource, 27 | ) 28 | } -------------------------------------------------------------------------------- /calf-ui/src/wasmJsMain/kotlin/com/mohamedrejeb/calf/ui/datepicker/AdaptiveDatePickerState.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.ui.datepicker 2 | 3 | import androidx.compose.material3.CalendarLocale 4 | 5 | internal actual fun getCalendarLocalDefault(): CalendarLocale = CalendarLocale.current 6 | -------------------------------------------------------------------------------- /calf-webview/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.internal.os.OperatingSystem 2 | 3 | plugins { 4 | id("compose.multiplatform") 5 | id("module.publication") 6 | } 7 | 8 | val os: OperatingSystem = OperatingSystem.current() 9 | val arch: String = System.getProperty("os.arch") 10 | val isAarch64: Boolean = arch.contains("aarch64") 11 | 12 | val platform = 13 | when { 14 | os.isWindows -> "win" 15 | os.isMacOsX -> "mac" 16 | else -> "linux" 17 | } + if (isAarch64) "-aarch64" else "" 18 | 19 | kotlin { 20 | sourceSets.commonMain.dependencies { 21 | implementation(compose.runtime) 22 | implementation(compose.foundation) 23 | implementation(compose.material3) 24 | implementation(libs.kotlinx.coroutines.core) 25 | } 26 | 27 | sourceSets.androidMain.dependencies { 28 | implementation(libs.activity.compose) 29 | implementation(libs.kotlinx.coroutines.android) 30 | } 31 | 32 | sourceSets.desktopMain.dependencies { 33 | implementation("org.openjfx:javafx-base:19:$platform") 34 | implementation("org.openjfx:javafx-graphics:19:$platform") 35 | implementation("org.openjfx:javafx-controls:19:$platform") 36 | implementation("org.openjfx:javafx-media:19:$platform") 37 | implementation("org.openjfx:javafx-web:19:$platform") 38 | implementation("org.openjfx:javafx-swing:19:$platform") 39 | implementation(libs.kotlinx.coroutines.swing) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf .idea 3 | ./gradlew clean 4 | rm -rf .gradle 5 | rm -rf build 6 | rm -rf */build 7 | rm -rf iosApp/iosApp.xcworkspace 8 | rm -rf iosApp/Pods 9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace 10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata 11 | -------------------------------------------------------------------------------- /convention-plugins/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | } 6 | 7 | java { 8 | sourceCompatibility = JavaVersion.VERSION_11 9 | targetCompatibility = JavaVersion.VERSION_11 10 | } 11 | 12 | tasks.withType { 13 | kotlinOptions { 14 | jvmTarget = "11" 15 | } 16 | } 17 | 18 | group = "com.mohamedrejeb.gradle" 19 | version = "0.1.0" 20 | 21 | dependencies { 22 | implementation(libs.gradlePlugin.android) 23 | implementation(libs.gradlePlugin.jetbrainsCompose) 24 | implementation(libs.gradlePlugin.kotlin) 25 | implementation(libs.gradlePlugin.composeCompiler) 26 | implementation(libs.nexus.publish) 27 | 28 | // hack to access version catalogue https://github.com/gradle/gradle/issues/15383 29 | compileOnly(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) 30 | } 31 | -------------------------------------------------------------------------------- /convention-plugins/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "convention-plugins" 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | gradlePluginPortal() 7 | mavenCentral() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | @Suppress("UnstableApiUsage") 13 | repositories { 14 | google() 15 | gradlePluginPortal() 16 | mavenCentral() 17 | } 18 | 19 | versionCatalogs { 20 | create("libs") { 21 | from(files("../gradle/libs.versions.toml")) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/Android.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.LibraryExtension 2 | import org.gradle.accessors.dm.LibrariesForLibs 3 | import org.gradle.api.JavaVersion 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.configure 6 | import org.gradle.kotlin.dsl.the 7 | 8 | fun Project.androidLibrarySetup() { 9 | val libs = the() 10 | 11 | extensions.configure { 12 | namespace = group.toString() + path.replace("-", "").split(":").joinToString(".") 13 | compileSdk = libs.versions.android.compileSdk.get().toInt() 14 | 15 | defaultConfig { 16 | minSdk = libs.versions.android.minSdk.get().toInt() 17 | } 18 | compileOptions { 19 | sourceCompatibility = JavaVersion.VERSION_1_8 20 | targetCompatibility = JavaVersion.VERSION_1_8 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/GradleSetupPlugin.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | 4 | class GradleSetupPlugin : Plugin { 5 | override fun apply(target: Project) {} 6 | } 7 | -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/Hirearchy.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.NamedDomainObjectContainer 2 | import org.gradle.api.NamedDomainObjectProvider 3 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 4 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 5 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet 6 | 7 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 8 | fun KotlinMultiplatformExtension.applyHierarchyTemplate() { 9 | applyDefaultHierarchyTemplate { 10 | common { 11 | group("material") { 12 | withAndroidTarget() 13 | withJvm() 14 | withJs() 15 | withWasmJs() 16 | } 17 | 18 | group("nonAndroid") { 19 | withJvm() 20 | withIos() 21 | withJs() 22 | withWasmJs() 23 | } 24 | 25 | group("web") { 26 | withJs() 27 | withWasmJs() 28 | } 29 | } 30 | } 31 | } 32 | 33 | val NamedDomainObjectContainer.desktopMain: NamedDomainObjectProvider 34 | get() = named("desktopMain") 35 | 36 | val NamedDomainObjectContainer.materialMain: NamedDomainObjectProvider 37 | get() = named("materialMain") 38 | 39 | val NamedDomainObjectContainer.nonAndroidMain: NamedDomainObjectProvider 40 | get() = named("nonAndroidMain") 41 | 42 | val NamedDomainObjectContainer.jsWasmMain: NamedDomainObjectProvider 43 | get() = named("jsWasmMain") -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/Targets.kt: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 4 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 5 | 6 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 7 | fun KotlinMultiplatformExtension.applyTargets() { 8 | androidTarget { 9 | publishLibraryVariants("release") 10 | compilerOptions { 11 | jvmTarget.set(JvmTarget.JVM_1_8) 12 | } 13 | } 14 | 15 | jvmToolchain(11) 16 | jvm("desktop") 17 | 18 | js().browser() 19 | 20 | @OptIn(ExperimentalWasmDsl::class) 21 | wasmJs().browser() 22 | 23 | iosX64() 24 | iosArm64() 25 | iosSimulatorArm64() 26 | } 27 | -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/android.library.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `android-library` 3 | } 4 | 5 | androidLibrarySetup() -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/compose.multiplatform.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kotlin.multiplatform") 3 | id("org.jetbrains.compose") 4 | `kotlin-composecompiler` 5 | } 6 | -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/kotlin.multiplatform.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-multiplatform` 3 | id("android.library") 4 | } 5 | 6 | kotlin { 7 | applyHierarchyTemplate() 8 | applyTargets() 9 | } -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/module.publication.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.publish.maven.MavenPublication 2 | import org.gradle.api.tasks.bundling.Jar 3 | import org.gradle.kotlin.dsl.`maven-publish` 4 | 5 | plugins { 6 | `maven-publish` 7 | signing 8 | } 9 | 10 | publishing { 11 | // Configure all publications 12 | publications.withType { 13 | // Stub javadoc.jar artifact 14 | artifact( 15 | tasks.register("${name}JavadocJar", Jar::class) { 16 | archiveClassifier.set("javadoc") 17 | archiveAppendix.set(this@withType.name) 18 | }, 19 | ) 20 | 21 | // Provide artifacts information required by Maven Central 22 | pom { 23 | name.set("Calf - Compose Adaptive Look & Feel") 24 | description.set("Calf is a library that allows you to easily create adaptive UIs for your Compose Multiplatform apps.") 25 | url.set("https://github.com/MohamedRejeb/Calf") 26 | 27 | licenses { 28 | license { 29 | name.set("Apache-2.0") 30 | url.set("https://opensource.org/licenses/Apache-2.0") 31 | } 32 | } 33 | developers { 34 | developer { 35 | id.set("MohamedRejeb") 36 | name.set("Mohamed Rejeb") 37 | email.set("mohamedrejeb445@gmail.com") 38 | } 39 | } 40 | issueManagement { 41 | system.set("Github") 42 | url.set("https://github.com/MohamedRejeb/Calf/issues") 43 | } 44 | scm { 45 | connection.set("https://github.com/MohamedRejeb/Calf.git") 46 | url.set("https://github.com/MohamedRejeb/Calf") 47 | } 48 | } 49 | } 50 | } 51 | 52 | signing { 53 | useInMemoryPgpKeys( 54 | System.getenv("OSSRH_GPG_SECRET_KEY_ID"), 55 | System.getenv("OSSRH_GPG_SECRET_KEY"), 56 | System.getenv("OSSRH_GPG_SECRET_KEY_PASSWORD"), 57 | ) 58 | sign(publishing.publications) 59 | } 60 | 61 | // TODO: remove after https://youtrack.jetbrains.com/issue/KT-46466 is fixed 62 | project.tasks.withType(AbstractPublishToMaven::class.java).configureEach { 63 | dependsOn(project.tasks.withType(Sign::class.java)) 64 | } 65 | -------------------------------------------------------------------------------- /convention-plugins/src/main/kotlin/root.publication.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("io.github.gradle-nexus.publish-plugin") 3 | } 4 | 5 | allprojects { 6 | group = "com.mohamedrejeb.calf" 7 | version = System.getenv("VERSION") ?: "0.8.0" 8 | } 9 | 10 | nexusPublishing { 11 | // Configure maven central repository 12 | // https://github.com/gradle-nexus/publish-plugin#publishing-to-maven-central-via-sonatype-ossrh 13 | repositories { 14 | sonatype { 15 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 16 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 17 | stagingProfileId.set(System.getenv("OSSRH_STAGING_PROFILE_ID")) 18 | username.set(System.getenv("OSSRH_USERNAME")) 19 | password.set(System.getenv("OSSRH_PASSWORD")) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/images/AdaptiveAlertDialog-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveAlertDialog-android.png -------------------------------------------------------------------------------- /docs/images/AdaptiveAlertDialog-ios-action-sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveAlertDialog-ios-action-sheet.png -------------------------------------------------------------------------------- /docs/images/AdaptiveAlertDialog-ios-with-text-field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveAlertDialog-ios-with-text-field.png -------------------------------------------------------------------------------- /docs/images/AdaptiveAlertDialog-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveAlertDialog-ios.png -------------------------------------------------------------------------------- /docs/images/AdaptiveBottomSheet-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveBottomSheet-android.png -------------------------------------------------------------------------------- /docs/images/AdaptiveBottomSheet-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveBottomSheet-ios.png -------------------------------------------------------------------------------- /docs/images/AdaptiveCircularProgressIndicator-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveCircularProgressIndicator-android.png -------------------------------------------------------------------------------- /docs/images/AdaptiveCircularProgressIndicator-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveCircularProgressIndicator-ios.png -------------------------------------------------------------------------------- /docs/images/AdaptiveDatePicker-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveDatePicker-android.png -------------------------------------------------------------------------------- /docs/images/AdaptiveDatePicker-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveDatePicker-ios.png -------------------------------------------------------------------------------- /docs/images/AdaptiveFilePicker-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveFilePicker-android.png -------------------------------------------------------------------------------- /docs/images/AdaptiveFilePicker-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveFilePicker-desktop.png -------------------------------------------------------------------------------- /docs/images/AdaptiveFilePicker-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveFilePicker-ios.png -------------------------------------------------------------------------------- /docs/images/AdaptiveFilePicker-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveFilePicker-web.png -------------------------------------------------------------------------------- /docs/images/AdaptiveTimePicker-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveTimePicker-android.png -------------------------------------------------------------------------------- /docs/images/AdaptiveTimePicker-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/AdaptiveTimePicker-ios.png -------------------------------------------------------------------------------- /docs/images/Permissions-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/Permissions-android.png -------------------------------------------------------------------------------- /docs/images/Permissions-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/Permissions-ios.png -------------------------------------------------------------------------------- /docs/images/WebView-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/WebView-android.png -------------------------------------------------------------------------------- /docs/images/WebView-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/WebView-ios.png -------------------------------------------------------------------------------- /docs/images/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/logo.ico -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/docs/images/thumbnail.png -------------------------------------------------------------------------------- /docs/ui.md: -------------------------------------------------------------------------------- 1 | # Adaptive UI 2 | 3 | ## Installation 4 | 5 | [![Maven Central](https://img.shields.io/maven-central/v/com.mohamedrejeb.calf/calf-ui)](https://search.maven.org/search?q=g:%22com.mohamedrejeb.calf%22%20AND%20a:%22calf-ui%22) 6 | 7 | | Kotlin version | Compose version | Calf version | 8 | |----------------|-----------------|--------------| 9 | | 2.1.21 | 1.8.0 | 0.8.0 | 10 | | 2.1.10 | 1.7.3 | 0.7.1 | 11 | | 2.1.0 | 1.7.3 | 0.7.0 | 12 | | 2.0.21 | 1.7.0 | 0.6.1 | 13 | | 2.0.10 | 1.6.11 | 0.5.5 | 14 | | 1.9.22 | 1.6.0 | 0.4.1 | 15 | | 1.9.21 | 1.5.11 | 0.3.1 | 16 | | 1.9.20 | 1.5.10 | 0.2.0 | 17 | | 1.9.0 | 1.5.0 | 0.1.1 | 18 | 19 | Add the following dependency to your module `build.gradle.kts` file: 20 | 21 | ```kotlin 22 | api("com.mohamedrejeb.calf:calf-ui:0.8.0") 23 | ``` 24 | 25 | If you are using `calf-ui` artifact, make sure to export it to binaries: 26 | 27 | #### Regular Framework 28 | ```kotlin 29 | kotlin { 30 | targets 31 | .filterIsInstance() 32 | .filter { it.konanTarget.family == Family.IOS } 33 | .forEach { 34 | it.binaries.framework { 35 | export("com.mohamedrejeb.calf:calf-ui:0.8.0") 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | #### CocoaPods 42 | ```kotlin 43 | kotlin { 44 | cocoapods { 45 | framework { 46 | export("com.mohamedrejeb.calf:calf-ui:0.8.0") 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | > **Important:** Exporting `calf-ui` to binaries is required to make it work on iOS. 53 | 54 | ## Components 55 | 56 | Calf UI provides a set of adaptive UI components that adapt to the platform they are running on. Here's a list of available components: 57 | 58 | - [AdaptiveAlertDialog](ui/adaptive-alert-dialog.md) - A dialog that adapts to the platform it is running on 59 | - [AdaptiveBottomSheet](ui/adaptive-bottom-sheet.md) - A bottom sheet that adapts to the platform it is running on 60 | - [AdaptiveCircularProgressIndicator](ui/adaptive-circular-progress-indicator.md) - A circular progress indicator that adapts to the platform it is running on 61 | - [AdaptiveClickable](ui/adaptive-clickable.md) - A clickable modifier that replaces indication on iOS with scaling effect 62 | - [AdaptiveDatePicker](ui/adaptive-date-picker.md) - A date picker that adapts to the platform it is running on 63 | - [AdaptiveTimePicker](ui/adaptive-time-picker.md) - A time picker that adapts to the platform it is running on 64 | -------------------------------------------------------------------------------- /docs/ui/adaptive-circular-progress-indicator.md: -------------------------------------------------------------------------------- 1 | # AdaptiveCircularProgressIndicator 2 | 3 | `AdaptiveCircularProgressIndicator` is a circular progress indicator that adapts to the platform it is running on. It is a wrapper around `CircularProgressIndicator` on Android, and it implements similar look to `UIActivityIndicatorView` on iOS. 4 | 5 | | Material (Android, Desktop, Web) | Cupertino (iOS) | 6 | |----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| 7 | | ![Circular Progress Indicator Android](../images/AdaptiveCircularProgressIndicator-android.png) | ![Circular Progress Indicator iOS](../images/AdaptiveCircularProgressIndicator-ios.png) | 8 | 9 | ```kotlin 10 | AdaptiveCircularProgressIndicator( 11 | modifier = Modifier.size(50.dp), 12 | color = Color.Red, 13 | ) 14 | ``` -------------------------------------------------------------------------------- /docs/ui/adaptive-clickable.md: -------------------------------------------------------------------------------- 1 | # AdaptiveClickable 2 | 3 | `.adaptiveClickable` is a clickable modifier that adapts to the platform it is running on. On Android, it behaves like the standard `.clickable` modifier with ripple indication, while on iOS, it replaces the indication with a scaling effect that matches iOS design patterns. 4 | 5 | ## Usage 6 | 7 | Use `.adaptiveClickable` when you want to provide a platform-specific click interaction: 8 | - On Android: Standard Material ripple effect 9 | - On iOS: Subtle scaling animation that follows iOS design guidelines 10 | 11 | ## Example 12 | 13 | ```kotlin 14 | Box( 15 | modifier = Modifier 16 | .size(100.dp) 17 | .background(Color.Red, RoundedCornerShape(8.dp)) 18 | .adaptiveClickable( 19 | // Optional parameters 20 | shape = RoundedCornerShape(8.dp), 21 | interactionSource = remember { MutableInteractionSource() }, 22 | indication = rememberRipple(), // Used on Android only 23 | enabled = true, 24 | ) { 25 | // Handle click 26 | println("Clicked!") 27 | } 28 | ) 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /docs/ui/adaptive-date-picker.md: -------------------------------------------------------------------------------- 1 | # AdaptiveDatePicker 2 | 3 | `AdaptiveDatePicker` is a date picker that adapts to the platform it is running on. It is a wrapper around `DatePicker` on Android and `UIDatePicker` on iOS, providing a native date selection experience on each platform. 4 | 5 | | Material (Android, Desktop, Web) | Cupertino (iOS) | 6 | |---------------------------------------------------------------|-------------------------------------------------------| 7 | | ![Date Picker Android](../images/AdaptiveDatePicker-android.png) | ![Date Picker iOS](../images/AdaptiveDatePicker-ios.png) | 8 | 9 | ## Usage 10 | 11 | The `AdaptiveDatePicker` uses a state object to manage and track the selected date. You can observe changes to the selected date through the state. 12 | 13 | ```kotlin 14 | // Create and remember the date picker state 15 | val state = rememberAdaptiveDatePickerState() 16 | 17 | // Optional: Set initial date (default is current date) 18 | LaunchedEffect(Unit) { 19 | state.setSelection(Calendar.getInstance().apply { 20 | set(2023, 0, 1) // January 1, 2023 21 | }.timeInMillis) 22 | } 23 | 24 | // React to date changes 25 | LaunchedEffect(state.selectedDateMillis) { 26 | val selectedDate = state.selectedDateMillis?.let { millis -> 27 | Calendar.getInstance().apply { 28 | timeInMillis = millis 29 | } 30 | } 31 | 32 | // Do something with the selected date 33 | selectedDate?.let { 34 | val year = it.get(Calendar.YEAR) 35 | val month = it.get(Calendar.MONTH) + 1 // Calendar months are 0-based 36 | val day = it.get(Calendar.DAY_OF_MONTH) 37 | println("Selected date: $year-$month-$day") 38 | } 39 | } 40 | 41 | // Display the date picker 42 | AdaptiveDatePicker( 43 | state = state, 44 | modifier = Modifier.fillMaxWidth(), 45 | // Optional: Customize date constraints 46 | dateValidator = { timestamp -> 47 | // Example: Only allow dates from today forward 48 | timestamp >= Calendar.getInstance().apply { 49 | set(Calendar.HOUR_OF_DAY, 0) 50 | set(Calendar.MINUTE, 0) 51 | set(Calendar.SECOND, 0) 52 | set(Calendar.MILLISECOND, 0) 53 | }.timeInMillis 54 | } 55 | ) 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /docs/ui/adaptive-time-picker.md: -------------------------------------------------------------------------------- 1 | # AdaptiveTimePicker 2 | 3 | `AdaptiveTimePicker` is a time picker that adapts to the platform it is running on. It is a wrapper around `TimePicker` on Android and `UIDatePicker` (in time mode) on iOS, providing a native time selection experience on each platform. 4 | 5 | | Material (Android, Desktop, Web) | Cupertino (iOS) | 6 | |---------------------------------------------------------------|-------------------------------------------------------| 7 | | ![Time Picker Android](../images/AdaptiveTimePicker-android.png) | ![Time Picker iOS](../images/AdaptiveTimePicker-ios.png) | 8 | 9 | ## Usage 10 | 11 | The `AdaptiveTimePicker` uses a state object to manage and track the selected time. You can observe changes to the hour and minute through the state. 12 | 13 | ```kotlin 14 | // Create and remember the time picker state 15 | val state = rememberAdaptiveTimePickerState() 16 | 17 | // Optional: Set initial time (default is current time) 18 | LaunchedEffect(Unit) { 19 | state.setHour(14) // 2 PM 20 | state.setMinute(30) // 30 minutes 21 | } 22 | 23 | // React to time changes 24 | LaunchedEffect(state.hour, state.minute) { 25 | // Format the time 26 | val hour = state.hour 27 | val minute = state.minute 28 | val formattedTime = String.format("%02d:%02d", hour, minute) 29 | 30 | // Do something with the selected time 31 | println("Selected time: $formattedTime") 32 | } 33 | 34 | // Display the time picker 35 | AdaptiveTimePicker( 36 | state = state, 37 | modifier = Modifier.fillMaxWidth(), 38 | // Optional: Configure time picker options 39 | is24Hour = true // Use 24-hour format instead of AM/PM 40 | ) 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /docs/webview.md: -------------------------------------------------------------------------------- 1 | # File Picker 2 | 3 | ## Installation 4 | 5 | [![Maven Central](https://img.shields.io/maven-central/v/com.mohamedrejeb.calf/calf-webview)](https://search.maven.org/search?q=g:%22com.mohamedrejeb.calf%22%20AND%20a:%calf-webview%22) 6 | 7 | Add the following dependency to your module `build.gradle.kts` file: 8 | 9 | ```kotlin 10 | implementation("com.mohamedrejeb.calf:calf-webview:0.8.0") 11 | ``` 12 | 13 | ## Usage 14 | 15 | `WebView` is a view that adapts to the platform it is running on. It is a wrapper around `WebView` on Android, `WKWebView` on iOS and JavaFX `WebView` on Desktop. 16 | 17 | | Android | iOS | 18 | |-------------------------------------------------|-----------------------------------------| 19 | | ![Web View Android](images/WebView-android.png) | ![Web View iOS](images/WebView-ios.png) | 20 | 21 | ```kotlin 22 | val state = rememberWebViewState( 23 | url = "https://github.com/MohamedRejeb" 24 | ) 25 | 26 | LaunchedEffect(state.isLoading) { 27 | // Get the current loading state 28 | } 29 | 30 | WebView( 31 | state = state, 32 | modifier = Modifier 33 | .fillMaxSize() 34 | ) 35 | ``` 36 | 37 | #### Web View Settings 38 | 39 | You can customize the web view settings by changing the `WebSettings` object in the `WebViewState`: 40 | 41 | ```kotlin 42 | val state = rememberWebViewState( 43 | url = "https://github.com/MohamedRejeb" 44 | ) 45 | 46 | LaunchedEffect(Unit) { 47 | // Enable JavaScript 48 | state.settings.javaScriptEnabled = true 49 | 50 | // Enable Zoom in Android 51 | state.settings.androidSettings.supportZoom = true 52 | } 53 | ``` 54 | 55 | #### Call JavaScript 56 | 57 | You can call JavaScript functions from the web view by using the `evaluateJavaScript` function: 58 | 59 | ```kotlin 60 | val state = rememberWebViewState( 61 | url = "https://github.com/MohamedRejeb" 62 | ) 63 | 64 | LaunchedEffect(Unit) { 65 | val jsCode = """ 66 | document.body.style.backgroundColor = "red"; 67 | document.title 68 | """.trimIndent() 69 | 70 | // Evaluate the JavaScript code 71 | state.evaluateJavaScript(jsCode) { 72 | // Do something with the result 73 | println("JS Response: $it") 74 | } 75 | } 76 | ``` 77 | 78 | > **Note:** The `evaluateJavaScript` method only works when you enable JavaScript in the web view settings. -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4048M" 3 | org.gradle.caching=true 4 | org.gradle.parallel=true 5 | 6 | kotlin.code.style=official 7 | 8 | #MPP 9 | kotlin.mpp.androidSourceSetLayoutVersion=2 10 | kotlin.mpp.androidGradlePluginCompatibility.nowarn=true 11 | kotlin.mpp.enableCInteropCommonization=true 12 | 13 | #Android 14 | android.useAndroidX=true 15 | android.nonTransitiveRClass=true 16 | 17 | #iOS 18 | org.jetbrains.compose.experimental.uikit.enabled=true 19 | 20 | #Js 21 | kotlin.js.compiler=ir 22 | org.jetbrains.compose.experimental.jscanvas.enabled=true 23 | 24 | #Wasm 25 | org.jetbrains.compose.experimental.wasm.enabled=true 26 | 27 | #macOS 28 | org.jetbrains.compose.experimental.macos.enabled=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /sample/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) 3 | alias(libs.plugins.kotlinAndroid) 4 | alias(libs.plugins.composeCompiler) 5 | alias(libs.plugins.composeMultiplatform) 6 | } 7 | 8 | android { 9 | namespace = "com.mohamedrejeb.calf.android" 10 | compileSdk = libs.versions.android.compileSdk.get().toInt() 11 | 12 | defaultConfig { 13 | minSdk = libs.versions.android.minSdk.get().toInt() 14 | targetSdk = libs.versions.android.compileSdk.get().toInt() 15 | 16 | applicationId = "com.mohamedrejeb.calf.android" 17 | versionCode = 1 18 | versionName = "1.0" 19 | } 20 | 21 | buildFeatures { 22 | compose = true 23 | } 24 | compileOptions { 25 | sourceCompatibility = JavaVersion.VERSION_1_8 26 | targetCompatibility = JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = "1.8" 30 | } 31 | packaging { 32 | resources { 33 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 34 | } 35 | } 36 | dependencies { 37 | implementation(projects.sample.common) 38 | 39 | implementation(libs.activity.compose) 40 | implementation(libs.compose.tooling.preview) 41 | implementation(libs.appcompat) 42 | debugImplementation(libs.compose.tooling) 43 | } 44 | } -------------------------------------------------------------------------------- /sample/android/src/main/java/com/mohamedrejeb/calf/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.activity.enableEdgeToEdge 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.ui.platform.LocalContext 9 | import androidx.core.view.WindowCompat 10 | 11 | class MainActivity : AppCompatActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | WindowCompat.setDecorFitsSystemWindows(window, false) 16 | enableEdgeToEdge() 17 | 18 | setContent { 19 | MaterialTheme { 20 | LocalContext.current 21 | App() 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/android/src/main/java/com/mohamedrejeb/calf/sample/PermissionScreenPreview.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.tooling.preview.Preview 5 | import com.mohamedrejeb.calf.sample.screens.PermissionScreen 6 | 7 | @Preview 8 | @Composable 9 | fun PermissionScreenPreview() { 10 | PermissionScreen( 11 | navigateBack = {} 12 | ) 13 | } -------------------------------------------------------------------------------- /sample/android/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /sample/common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("compose.multiplatform") 3 | } 4 | 5 | kotlin { 6 | listOf( 7 | iosX64(), 8 | iosArm64(), 9 | iosSimulatorArm64(), 10 | ).forEach { iosTarget -> 11 | iosTarget.binaries.framework { 12 | baseName = "Common" 13 | isStatic = true 14 | 15 | // IMPORTANT: Exporting calf-ui is required for some functionalities to work 16 | export(projects.calfUi) 17 | } 18 | } 19 | 20 | sourceSets.commonMain.dependencies { 21 | api(compose.runtime) 22 | api(compose.foundation) 23 | api(compose.material) 24 | api(compose.material3) 25 | implementation(compose.materialIconsExtended) 26 | 27 | implementation(libs.kotlinx.datetime) 28 | 29 | // Calf 30 | api(projects.calfUi) 31 | implementation(projects.calfFilePicker) 32 | implementation(projects.calfFilePickerCoil) 33 | implementation(projects.calfPermissions) 34 | implementation(projects.calfWebview) 35 | implementation(libs.compose.navigation) 36 | implementation(projects.calfNavigation) 37 | implementation(projects.calfCameraPicker) 38 | 39 | // Coil 40 | implementation(libs.coil.compose) 41 | implementation(libs.coil.network.ktor) 42 | 43 | // Ktor 44 | implementation(libs.ktor.client.core) 45 | } 46 | 47 | sourceSets.androidMain.dependencies { 48 | implementation(libs.kotlinx.coroutines.android) 49 | implementation(libs.ktor.client.okhttp) 50 | } 51 | 52 | sourceSets.desktopMain.dependencies { 53 | implementation(compose.desktop.currentOs) 54 | 55 | implementation(libs.kotlinx.coroutines.swing) 56 | implementation(libs.ktor.client.okhttp) 57 | } 58 | 59 | sourceSets.iosMain.dependencies { 60 | implementation(libs.ktor.client.darwin) 61 | } 62 | 63 | sourceSets.jsMain.dependencies { 64 | implementation(libs.ktor.client.js) 65 | } 66 | 67 | sourceSets.wasmJsMain.dependencies { 68 | implementation(libs.ktor.client.wasm) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sample/common/src/androidMain/kotlin/com.mohamedrejeb.calf.sample/Platform.android.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | actual val currentPlatform: Platform = Platform.Android -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/App.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Surface 5 | import androidx.compose.runtime.Composable 6 | import com.mohamedrejeb.calf.navigation.rememberNavController 7 | import com.mohamedrejeb.calf.sample.navigation.AppNavGraph 8 | import com.mohamedrejeb.calf.sample.ui.theme.CalfTheme 9 | 10 | @Composable 11 | fun App() = 12 | CalfTheme { 13 | val navController = rememberNavController() 14 | 15 | Surface( 16 | color = MaterialTheme.colorScheme.surface, 17 | contentColor = MaterialTheme.colorScheme.onSurface, 18 | ) { 19 | AppNavGraph(navController = navController) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | enum class Platform { 4 | Android, 5 | IOS, 6 | Desktop, 7 | Web; 8 | 9 | val isAndroid: Boolean 10 | get() = this == Android 11 | 12 | val isIOS: Boolean 13 | 14 | get() = this == IOS 15 | 16 | val isDesktop: Boolean 17 | get() = this == Desktop 18 | 19 | val isWeb: Boolean 20 | get() = this == Web 21 | } 22 | 23 | expect val currentPlatform: Platform -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/coil/ImageLoader.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.coil 2 | 3 | import androidx.compose.runtime.Composable 4 | import coil3.ImageLoader 5 | import coil3.PlatformContext 6 | import coil3.annotation.ExperimentalCoilApi 7 | import coil3.memory.MemoryCache 8 | import coil3.network.ktor3.KtorNetworkFetcherFactory 9 | import coil3.request.crossfade 10 | import coil3.util.DebugLogger 11 | import com.mohamedrejeb.calf.picker.coil.KmpFileFetcher 12 | 13 | @OptIn(ExperimentalCoilApi::class) 14 | @Composable 15 | fun setSingletonImageLoaderFactory() { 16 | coil3.compose.setSingletonImageLoaderFactory { 17 | newImageLoader( 18 | context = it, 19 | debug = false, 20 | ) 21 | } 22 | } 23 | 24 | private fun newImageLoader( 25 | context: PlatformContext, 26 | debug: Boolean = false, 27 | ): ImageLoader { 28 | return ImageLoader.Builder(context) 29 | .components { 30 | add(KtorNetworkFetcherFactory()) 31 | 32 | // Add a custom fetcher for loading KmpFile objects. 33 | add(KmpFileFetcher.Factory()) 34 | } 35 | .memoryCache { 36 | MemoryCache.Builder() 37 | // Set the max size to 25% of the app's available memory. 38 | .maxSizePercent(context, percent = 0.25) 39 | .build() 40 | } 41 | // Show a short crossfade when loading images asynchronously. 42 | .crossfade(true) 43 | // Enable logging if this is a debug build. 44 | .apply { 45 | if (debug) { 46 | logger(DebugLogger()) 47 | } 48 | } 49 | .build() 50 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/navigation/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.navigation 2 | 3 | enum class Screen { 4 | Home, 5 | Dialog, 6 | BottomSheet, 7 | AdaptiveClickable, 8 | DatePicker, 9 | TimePicker, 10 | ProgressBar, 11 | Switch, 12 | FilePicker, 13 | ImagePicker, 14 | CameraPickerScreen, 15 | WebView, 16 | Permission, 17 | Map, 18 | DropDownMenu, 19 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/AdaptiveClickableScreen.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.* 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.unit.dp 13 | import com.mohamedrejeb.calf.ui.gesture.adaptiveClickable 14 | 15 | @Composable 16 | fun AdaptiveClickableScreen( 17 | navigateBack: () -> Unit 18 | ) { 19 | Box( 20 | modifier = Modifier 21 | .fillMaxSize() 22 | .background(MaterialTheme.colorScheme.background) 23 | .windowInsetsPadding(WindowInsets.systemBars) 24 | .windowInsetsPadding(WindowInsets.ime) 25 | ) { 26 | IconButton( 27 | onClick = { 28 | navigateBack() 29 | }, 30 | modifier = Modifier 31 | .align(Alignment.TopStart) 32 | .padding(16.dp) 33 | ) { 34 | Icon( 35 | Icons.Filled.ArrowBackIosNew, 36 | contentDescription = "Back", 37 | tint = MaterialTheme.colorScheme.onBackground, 38 | ) 39 | } 40 | 41 | Box( 42 | modifier = Modifier 43 | .align(Alignment.Center) 44 | .adaptiveClickable( 45 | shape = MaterialTheme.shapes.medium, 46 | ) { 47 | // Handle click 48 | } 49 | .background(MaterialTheme.colorScheme.primary) 50 | .padding( 51 | horizontal = 20.dp, 52 | vertical = 10.dp 53 | ) 54 | ) { 55 | Text( 56 | text = "Adaptive Clickable", 57 | color = MaterialTheme.colorScheme.onPrimary, 58 | ) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/DatePickerScreen.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.gestures.Orientation 5 | import androidx.compose.foundation.gestures.scrollable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.* 10 | import androidx.compose.material3.* 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | import com.mohamedrejeb.calf.ui.datepicker.AdaptiveDatePicker 16 | import com.mohamedrejeb.calf.ui.datepicker.rememberAdaptiveDatePickerState 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun DatePickerScreen( 21 | navigateBack: () -> Unit 22 | ) { 23 | val state = rememberAdaptiveDatePickerState() 24 | 25 | Column( 26 | horizontalAlignment = Alignment.CenterHorizontally, 27 | modifier = Modifier 28 | .fillMaxSize() 29 | .background(MaterialTheme.colorScheme.background) 30 | .windowInsetsPadding(WindowInsets.systemBars) 31 | .windowInsetsPadding(WindowInsets.ime) 32 | .scrollable( 33 | rememberScrollState(), 34 | orientation = Orientation.Vertical 35 | ) 36 | ) { 37 | IconButton( 38 | onClick = { 39 | navigateBack() 40 | }, 41 | modifier = Modifier 42 | .align(Alignment.Start) 43 | .padding(16.dp) 44 | ) { 45 | Icon( 46 | Icons.Filled.ArrowBackIosNew, 47 | contentDescription = "Back", 48 | tint = MaterialTheme.colorScheme.onBackground, 49 | ) 50 | } 51 | 52 | Text( 53 | text = "Adaptive Date Picker", 54 | style = MaterialTheme.typography.titleLarge, 55 | modifier = Modifier 56 | .padding(16.dp) 57 | ) 58 | 59 | Text( 60 | text = "Selected date millis: ${state.selectedDateMillis}", 61 | style = MaterialTheme.typography.titleMedium, 62 | modifier = Modifier 63 | .padding(16.dp) 64 | ) 65 | 66 | AdaptiveDatePicker( 67 | state = state, 68 | colors = DatePickerDefaults.colors( 69 | containerColor = MaterialTheme.colorScheme.surface 70 | ), 71 | modifier = Modifier 72 | ) 73 | } 74 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/DropDownMenuScreen.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.* 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun DropDownMenuScreen( 15 | navigateBack: () -> Unit 16 | ) { 17 | Box( 18 | modifier = Modifier 19 | .fillMaxSize() 20 | .background(MaterialTheme.colorScheme.background) 21 | .windowInsetsPadding(WindowInsets.systemBars) 22 | .windowInsetsPadding(WindowInsets.ime) 23 | ) { 24 | IconButton( 25 | onClick = { 26 | navigateBack() 27 | }, 28 | colors = IconButtonDefaults.iconButtonColors( 29 | contentColor = MaterialTheme.colorScheme.onBackground, 30 | containerColor = MaterialTheme.colorScheme.surface, 31 | ), 32 | modifier = Modifier 33 | .align(Alignment.TopStart) 34 | .padding(16.dp) 35 | ) { 36 | Icon( 37 | Icons.Filled.ArrowBackIosNew, 38 | contentDescription = "Back", 39 | tint = MaterialTheme.colorScheme.onBackground, 40 | ) 41 | } 42 | 43 | var expanded by remember { mutableStateOf(false) } 44 | 45 | Row( 46 | verticalAlignment = Alignment.CenterVertically, 47 | modifier = Modifier 48 | .align(Alignment.Center) 49 | .padding(16.dp) 50 | ) { 51 | Text("Options") 52 | Box( 53 | modifier = Modifier 54 | ) { 55 | IconButton(onClick = { expanded = !expanded }) { 56 | Icon( 57 | imageVector = Icons.Default.MoreVert, 58 | contentDescription = "More" 59 | ) 60 | } 61 | 62 | // AdaptiveDropdownMenu( 63 | // expanded = expanded, 64 | // onDismissRequest = { expanded = false } 65 | // ) 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/ProgressBarScreen.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.* 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import com.mohamedrejeb.calf.ui.progress.AdaptiveCircularProgressIndicator 13 | 14 | @Composable 15 | fun ProgressBarScreen( 16 | navigateBack: () -> Unit 17 | ) { 18 | Box( 19 | modifier = Modifier 20 | .fillMaxSize() 21 | .background(MaterialTheme.colorScheme.background) 22 | .windowInsetsPadding(WindowInsets.systemBars) 23 | .windowInsetsPadding(WindowInsets.ime) 24 | ) { 25 | IconButton( 26 | onClick = { 27 | navigateBack() 28 | }, 29 | modifier = Modifier 30 | .align(Alignment.TopStart) 31 | .padding(16.dp) 32 | ) { 33 | Icon( 34 | Icons.Filled.ArrowBackIosNew, 35 | contentDescription = "Back", 36 | tint = MaterialTheme.colorScheme.onBackground, 37 | ) 38 | } 39 | 40 | Column( 41 | horizontalAlignment = Alignment.CenterHorizontally, 42 | verticalArrangement = Arrangement.spacedBy(16.dp), 43 | modifier = Modifier 44 | .align(Alignment.Center) 45 | ) { 46 | AdaptiveCircularProgressIndicator() 47 | AdaptiveCircularProgressIndicator(modifier = Modifier.size(60.dp)) 48 | AdaptiveCircularProgressIndicator(modifier = Modifier.size(90.dp)) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/SwitchScreen.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.* 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import com.mohamedrejeb.calf.ui.toggle.AdaptiveSwitch 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | fun SwitchScreen( 17 | navigateBack: () -> Unit 18 | ) { 19 | Box( 20 | modifier = Modifier 21 | .fillMaxSize() 22 | .background(MaterialTheme.colorScheme.background) 23 | .windowInsetsPadding(WindowInsets.systemBars) 24 | .windowInsetsPadding(WindowInsets.ime) 25 | ) { 26 | IconButton( 27 | onClick = { 28 | navigateBack() 29 | }, 30 | modifier = Modifier 31 | .align(Alignment.TopStart) 32 | .padding(16.dp) 33 | ) { 34 | Icon( 35 | Icons.Filled.ArrowBackIosNew, 36 | contentDescription = "Back", 37 | tint = MaterialTheme.colorScheme.onBackground, 38 | ) 39 | } 40 | 41 | Column( 42 | horizontalAlignment = Alignment.CenterHorizontally, 43 | verticalArrangement = Arrangement.spacedBy(16.dp), 44 | modifier = Modifier 45 | .align(Alignment.Center) 46 | ) { 47 | val firstSwitchState = remember { mutableStateOf(false) } 48 | AdaptiveSwitch( 49 | checked = firstSwitchState.value, 50 | onCheckedChange = { firstSwitchState.value = it }, 51 | modifier = Modifier 52 | .padding(16.dp) 53 | ) 54 | val secondSwitchState = remember { mutableStateOf(true) } 55 | AdaptiveSwitch( 56 | checked = secondSwitchState.value, 57 | onCheckedChange = { secondSwitchState.value = it }, 58 | modifier = Modifier 59 | .padding(16.dp) 60 | ) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/screens/TimePickerScreen.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.* 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import com.mohamedrejeb.calf.ui.timepicker.AdaptiveTimePicker 13 | import com.mohamedrejeb.calf.ui.timepicker.rememberAdaptiveTimePickerState 14 | 15 | @OptIn(ExperimentalMaterial3Api::class) 16 | @Composable 17 | fun TimePickerScreen( 18 | navigateBack: () -> Unit 19 | ) { 20 | val state = rememberAdaptiveTimePickerState( 21 | initialHour = 19, 22 | initialMinute = 20, 23 | ) 24 | 25 | Column( 26 | horizontalAlignment = Alignment.CenterHorizontally, 27 | modifier = Modifier 28 | .fillMaxSize() 29 | .background(MaterialTheme.colorScheme.background) 30 | .windowInsetsPadding(WindowInsets.systemBars) 31 | .windowInsetsPadding(WindowInsets.ime) 32 | ) { 33 | IconButton( 34 | onClick = { 35 | navigateBack() 36 | }, 37 | modifier = Modifier 38 | .align(Alignment.Start) 39 | .padding(16.dp) 40 | ) { 41 | Icon( 42 | Icons.Filled.ArrowBackIosNew, 43 | contentDescription = "Back", 44 | tint = MaterialTheme.colorScheme.onBackground, 45 | ) 46 | } 47 | 48 | Text( 49 | text = "Adaptive Time Picker", 50 | style = MaterialTheme.typography.titleLarge, 51 | modifier = Modifier 52 | .padding(16.dp) 53 | ) 54 | 55 | Text( 56 | text = "Selected time: ${state.hour}:${state.minute}", 57 | style = MaterialTheme.typography.titleMedium, 58 | modifier = Modifier 59 | .padding(16.dp) 60 | ) 61 | 62 | AdaptiveTimePicker( 63 | state = state, 64 | colors = TimePickerDefaults.colors( 65 | containerColor = MaterialTheme.colorScheme.surface 66 | ), 67 | modifier = Modifier 68 | ) 69 | } 70 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/ui.theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Blue400 = Color(0xFF4572E8) 6 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/ui.theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Color 9 | import com.mohamedrejeb.calf.sample.coil.setSingletonImageLoaderFactory 10 | 11 | private val DarkColorScheme = 12 | darkColorScheme( 13 | primary = Blue400, 14 | primaryContainer = Blue400, 15 | onPrimaryContainer = Color.White, 16 | background = Color.Black, 17 | surface = Color.Black, 18 | onPrimary = Color.White, 19 | onBackground = Color.White, 20 | onSurface = Color.White, 21 | ) 22 | 23 | private val LightColorScheme = 24 | lightColorScheme( 25 | primary = Blue400, 26 | primaryContainer = Blue400, 27 | onPrimaryContainer = Color.White, 28 | background = Color.White, 29 | surface = Color.White, 30 | onPrimary = Color.White, 31 | onBackground = Color.Black, 32 | onSurface = Color.Black, 33 | ) 34 | 35 | @Composable 36 | internal fun CalfTheme( 37 | darkTheme: Boolean = isSystemInDarkTheme(), 38 | content: @Composable () -> Unit, 39 | ) { 40 | setSingletonImageLoaderFactory() 41 | 42 | val colorScheme = 43 | if (darkTheme) 44 | DarkColorScheme 45 | else 46 | LightColorScheme 47 | 48 | MaterialTheme( 49 | colorScheme = colorScheme, 50 | typography = Typography, 51 | content = content, 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com.mohamedrejeb.calf.sample/ui.theme/Typography.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | val Typography = 6 | Typography( 7 | /* Other default text styles to override 8 | button = TextStyle( 9 | fontFamily = FontFamily.Default, 10 | fontWeight = FontWeight.W500, 11 | fontSize = 14.sp 12 | ), 13 | caption = TextStyle( 14 | fontFamily = FontFamily.Default, 15 | fontWeight = FontWeight.Normal, 16 | fontSize = 12.sp 17 | ) 18 | */ 19 | ) 20 | -------------------------------------------------------------------------------- /sample/common/src/desktopMain/kotlin/com.mohamedrejeb.calf.sample/Platform.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | actual val currentPlatform: Platform = Platform.Desktop -------------------------------------------------------------------------------- /sample/common/src/iosMain/kotlin/Main.ios.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import com.mohamedrejeb.calf.sample.App 3 | import platform.UIKit.UIViewController 4 | 5 | fun MainViewController(): UIViewController { 6 | // lateinit var uiViewController: UIViewController 7 | // val uiNavigationController: UINavigationController by lazy { 8 | // UINavigationController(rootViewController = uiViewController) 9 | // } 10 | 11 | // uiViewController = ComposeUIViewController { 12 | // App( 13 | // navigate = { 14 | // println("kotlin navigationController") 15 | // println("kotlin navigationController") 16 | // println(uiViewController) 17 | // println(uiViewController.navigationController) 18 | //// navigate() 19 | // uiViewController.navigationController?.pushViewController( 20 | // SecondViewController(), 21 | // true 22 | // ) 23 | // } 24 | // ) 25 | // } 26 | 27 | // savedUiViewController = uiViewController 28 | 29 | return ComposeUIViewController { 30 | App() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sample/common/src/iosMain/kotlin/com.mohamedrejeb.calf.sample/Platform.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | actual val currentPlatform: Platform = Platform.IOS -------------------------------------------------------------------------------- /sample/common/src/jsMain/kotlin/com.mohamedrejeb.calf.sample/Platform.js.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | actual val currentPlatform: Platform = Platform.Web -------------------------------------------------------------------------------- /sample/common/src/wasmJsMain/kotlin/com.mohamedrejeb.calf.sample/Platform.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.mohamedrejeb.calf.sample 2 | 3 | actual val currentPlatform: Platform = Platform.Web -------------------------------------------------------------------------------- /sample/desktop/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.composeCompiler) 6 | alias(libs.plugins.composeMultiplatform) 7 | } 8 | 9 | kotlin { 10 | jvmToolchain(11) 11 | 12 | jvm { 13 | withJava() 14 | } 15 | 16 | sourceSets.jvmMain.dependencies { 17 | implementation(projects.sample.common) 18 | implementation(compose.desktop.currentOs) 19 | } 20 | } 21 | 22 | compose.desktop { 23 | application { 24 | mainClass = "MainKt" 25 | nativeDistributions { 26 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 27 | packageName = "Calf" 28 | packageVersion = "1.0.0" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample/desktop/src/jvmMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.Window 2 | import androidx.compose.ui.window.application 3 | import com.mohamedrejeb.calf.sample.App 4 | 5 | 6 | fun main() { 7 | System.setProperty("compose.interop.blending", "true") 8 | 9 | application { 10 | Window( 11 | title = "Calf", 12 | onCloseRequest = ::exitApplication 13 | ) { 14 | App() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/ios/Calf.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/ios/Calf.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample/ios/Calf/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sample/ios/Calf/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/ios/Calf/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /sample/ios/Calf/CalfApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalfApp.swift 3 | // Calf 4 | // 5 | // Created by Mohamed Ben Rejeb on 2/8/2023. 6 | // 7 | 8 | import SwiftUI 9 | import Common 10 | 11 | @main 12 | struct CalfApp: App { 13 | var body: some Scene { 14 | WindowGroup { 15 | ComposeView() 16 | } 17 | } 18 | } 19 | 20 | struct ComposeView: UIViewControllerRepresentable { 21 | func makeUIViewController(context: Context) -> UIViewController { 22 | return Main_iosKt.MainViewController() 23 | } 24 | 25 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) { 26 | // Updates the state of the specified view controller with new information from SwiftUI. 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample/ios/Calf/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Calf 4 | // 5 | // Created by Mohamed Ben Rejeb on 2/8/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "globe") 14 | .imageScale(.large) 15 | .foregroundColor(.accentColor) 16 | Text("Hello, world!") 17 | } 18 | .padding() 19 | } 20 | } 21 | 22 | struct ContentView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | ContentView() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/ios/Calf/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | NSUserNotificationsUsageDescription 8 | Just for tests 9 | NSCameraUsageDescription 10 | Just for tests 11 | NSPhotoLibraryUsageDescription 12 | Just for tests 13 | NSLocationWhenInUseUsageDescription 14 | Just for tests 15 | NSMicrophoneUsageDescription 16 | Just for tests 17 | NSBluetoothAlwaysUsageDescription 18 | Just for tests 19 | NSBluetoothPeripheralUsageDescription 20 | Just for tests 21 | NSContactsUsageDescription 22 | Just for tests 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/ios/Calf/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /sample/web-js/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinMultiplatform) 3 | alias(libs.plugins.composeCompiler) 4 | alias(libs.plugins.composeMultiplatform) 5 | } 6 | 7 | kotlin { 8 | js { 9 | outputModuleName = "web" 10 | browser { 11 | commonWebpackConfig { 12 | outputFileName = "web.js" 13 | } 14 | } 15 | binaries.executable() 16 | } 17 | 18 | sourceSets.jsMain.dependencies { 19 | implementation(projects.sample.common) 20 | 21 | implementation(compose.runtime) 22 | implementation(compose.foundation) 23 | implementation(compose.ui) 24 | } 25 | } -------------------------------------------------------------------------------- /sample/web-js/src/jsMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.Box 2 | import androidx.compose.foundation.layout.fillMaxSize 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.window.CanvasBasedWindow 6 | import com.mohamedrejeb.calf.sample.App 7 | import org.jetbrains.skiko.wasm.onWasmReady 8 | 9 | @OptIn(ExperimentalComposeUiApi::class) 10 | fun main() { 11 | onWasmReady { 12 | CanvasBasedWindow( 13 | title = "Calf", 14 | canvasElementId = "ComposeTarget", 15 | ) { 16 | Box( 17 | modifier = Modifier.fillMaxSize(), 18 | ) { 19 | App() 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/web-js/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Calf 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/web-js/webpack.config.d/fs.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedRejeb/Calf/c181a829b8e477f8ffad3e78f9d518a0a0e08de5/sample/web-js/webpack.config.d/fs.js -------------------------------------------------------------------------------- /sample/web-wasm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.composeCompiler) 6 | alias(libs.plugins.composeMultiplatform) 7 | } 8 | 9 | kotlin { 10 | @OptIn(ExperimentalWasmDsl::class) 11 | wasmJs { 12 | outputModuleName = "web" 13 | browser { 14 | commonWebpackConfig { 15 | outputFileName = "web.js" 16 | } 17 | } 18 | binaries.executable() 19 | } 20 | 21 | sourceSets.commonMain.dependencies { 22 | implementation(projects.sample.common) 23 | 24 | implementation(compose.runtime) 25 | implementation(compose.foundation) 26 | implementation(compose.ui) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample/web-wasm/src/wasmJsMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.Box 2 | import androidx.compose.foundation.layout.fillMaxSize 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.window.CanvasBasedWindow 6 | import com.mohamedrejeb.calf.sample.App 7 | 8 | @OptIn(ExperimentalComposeUiApi::class) 9 | fun main() { 10 | CanvasBasedWindow( 11 | title = "Calf", 12 | canvasElementId = "ComposeTarget", 13 | ) { 14 | Box( 15 | modifier = Modifier.fillMaxSize(), 16 | ) { 17 | App() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/web-wasm/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Calf 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Calf" 2 | 3 | pluginManagement { 4 | includeBuild("convention-plugins") 5 | repositories { 6 | google { 7 | mavenContent { 8 | releasesOnly() 9 | } 10 | } 11 | mavenCentral() 12 | gradlePluginPortal() 13 | } 14 | } 15 | 16 | dependencyResolutionManagement { 17 | @Suppress("UnstableApiUsage") 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | plugins { 25 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 26 | } 27 | 28 | include( 29 | // Artifact modules 30 | ":calf-core", 31 | ":calf-ui", 32 | ":calf-webview", 33 | ":calf-sf-symbols", 34 | ":calf-navigation", 35 | ":calf-io", 36 | ":calf-file-picker", 37 | ":calf-file-picker-coil", 38 | ":calf-permissions", 39 | ":calf-geo", 40 | ":calf-maps", 41 | // Sample modules 42 | ":sample:android", 43 | ":sample:desktop", 44 | ":sample:web-js", 45 | ":sample:web-wasm", 46 | ":sample:common", 47 | "calf-camera-picker", 48 | ) 49 | 50 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 51 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 52 | --------------------------------------------------------------------------------