├── .editorconfig
├── .github
└── workflows
│ ├── build.yml
│ └── gradle-wrapper.yaml
├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
└── inspectionProfiles
│ ├── ktlint.xml
│ └── profiles_settings.xml
├── APISummaryFormat.md
├── CLAUDE.md
├── CodeCommentCleanup.md
├── DroidconIosDocumentation.md
├── IOSDEV.md
├── LICENSE.txt
├── MEDIA.md
├── README.md
├── StartupANRIssue.md
├── StructuredInstructionFormats.md
├── TimeZonesAndroid.txt
├── TimeZonesiOS.txt
├── android
├── .gitignore
├── build.gradle.kts
├── mock-google-services.json
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ └── co
│ │ └── touchlab
│ │ └── droidcon
│ │ └── android
│ │ ├── MainActivity.kt
│ │ ├── MainApp.kt
│ │ ├── service
│ │ └── impl
│ │ │ ├── AndroidAnalyticsService.kt
│ │ │ ├── DefaultFirebaseMessagingService.kt
│ │ │ └── DefaultParseUrlViewService.kt
│ │ └── util
│ │ └── NotificationLocalizedStringFactory.kt
│ ├── lint.xml
│ └── res
│ ├── drawable-night
│ ├── about_droidcon.xml
│ └── about_kotlin.xml
│ ├── drawable-nodpi
│ └── about_touchlab.png
│ ├── drawable
│ ├── about_droidcon.xml
│ ├── about_kotlin.xml
│ ├── ic_baseline_insert_invitation_24.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_splash_screen.xml
│ ├── linkedin.xml
│ └── twitter.xml
│ ├── mipmap-anydpi-v26
│ └── ic_launcher.xml
│ ├── values-night
│ └── colors.xml
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
├── build.gradle.kts
├── docs
└── HyperDrivev1.md
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── ios
├── Droidcon
│ ├── .gitignore
│ ├── Droidcon.xcodeproj
│ │ ├── project.pbxproj
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Droidcon.xcscheme
│ └── Droidcon
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── Accent.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ └── dcLondon24-Icon.png
│ │ ├── AttendButton.colorset
│ │ │ └── Contents.json
│ │ ├── AttendButton_Foreground.colorset
│ │ │ └── Contents.json
│ │ ├── Attending
│ │ │ ├── AttendingConflict.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AttendingNormal.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AttendingPast.colorset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── Divider.colorset
│ │ │ └── Contents.json
│ │ ├── ElevatedBackground.colorset
│ │ │ └── Contents.json
│ │ ├── ElevatedBackgroundDisabled.colorset
│ │ │ └── Contents.json
│ │ ├── ElevatedHeaderBackground.colorset
│ │ │ └── Contents.json
│ │ ├── Feedback
│ │ │ ├── Contents.json
│ │ │ ├── Feedback_Dissatisfied.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── baseline_sentiment_very_dissatisfied_black_48dp.png
│ │ │ ├── Feedback_Normal.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── baseline_sentiment_satisfied_black_48dp.png
│ │ │ └── Feedback_Satisfied.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── baseline_sentiment_satisfied_alt_black_48dp.png
│ │ ├── LaunchScreen
│ │ │ ├── Contents.json
│ │ │ ├── LaunchScreen_Background.colorset
│ │ │ │ └── Contents.json
│ │ │ └── LaunchScreen_Icon.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── London 24-Splash screen.svg
│ │ ├── Logos
│ │ │ ├── Contents.json
│ │ │ ├── about_droidcon.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── droidcon_logo_light.pdf
│ │ │ │ └── droidcon_logo_night.pdf
│ │ │ ├── about_kotlin.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── small Kotlin Full Color Logo on Black RGB.svg
│ │ │ │ └── small Kotlin Full Color Logo on White RGB.svg
│ │ │ ├── about_touchlab.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── TL_Gradient.png
│ │ │ ├── linkedin.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── linked-in.png
│ │ │ └── twitter.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── twitter.png
│ │ ├── NavBar
│ │ │ ├── Contents.json
│ │ │ ├── NavBar_Background.colorset
│ │ │ │ └── Contents.json
│ │ │ └── NavBar_Foreground.colorset
│ │ │ │ └── Contents.json
│ │ ├── Shadow.colorset
│ │ │ └── Contents.json
│ │ ├── TabBar
│ │ │ ├── Contents.json
│ │ │ ├── TabBar_Background.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── TabBar_Foreground.colorset
│ │ │ │ └── Contents.json
│ │ │ └── TabBar_Foreground_Selected.colorset
│ │ │ │ └── Contents.json
│ │ └── TextFieldBackground.colorset
│ │ │ └── Contents.json
│ │ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ │ ├── ComposeController.swift
│ │ ├── Droidcon.entitlements
│ │ ├── DroidconApp.swift
│ │ ├── IOSAnalyticsService.swift
│ │ ├── Info.plist
│ │ ├── Koin.swift
│ │ ├── Settings.bundle
│ │ ├── Root.plist
│ │ └── en.lproj
│ │ │ └── Root.strings
│ │ ├── Utils
│ │ └── LifecycleManager.swift
│ │ └── en.lproj
│ │ └── Localizable.strings
├── build.gradle.kts
└── src
│ └── iosMain
│ └── kotlin
│ └── co
│ └── touchlab
│ └── droidcon
│ └── ios
│ ├── DependencyInjection.kt
│ ├── service
│ └── DefaultParseUrlViewService.kt
│ └── util
│ ├── NotificationLocalizedStringFactory.kt
│ └── formatter
│ └── IOSDateFormatter.kt
├── privacy.html
├── settings.gradle.kts
├── shared-ui
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ └── co
│ │ │ └── touchlab
│ │ │ └── droidcon
│ │ │ └── ui
│ │ │ ├── settings
│ │ │ └── PlatformSpecificSettings.kt
│ │ │ ├── theme
│ │ │ └── Type.android.kt
│ │ │ └── util
│ │ │ ├── Dialog.kt
│ │ │ ├── LocalImage.jvm.kt
│ │ │ ├── MainView.kt
│ │ │ └── NavigationBackPressWrapper.kt
│ └── res
│ │ └── font
│ │ ├── montserrat_bold.ttf
│ │ ├── montserrat_light.ttf
│ │ ├── montserrat_medium.ttf
│ │ ├── montserrat_regular.ttf
│ │ └── montserrat_semi_bold.ttf
│ ├── commonMain
│ ├── composeResources
│ │ └── drawable
│ │ │ └── venue-map-1.jpg
│ └── kotlin
│ │ └── co.touchlab.droidcon
│ │ ├── ui
│ │ ├── BottomNavigationView.kt
│ │ ├── FeedbackDialog.kt
│ │ ├── FirstRunConferenceSelector.kt
│ │ ├── MainComposeView.kt
│ │ ├── UiModule.kt
│ │ ├── session
│ │ │ ├── SessionBlockView.kt
│ │ │ ├── SessionDetailView.kt
│ │ │ ├── SessionListView.kt
│ │ │ └── SpeakerDetailView.kt
│ │ ├── settings
│ │ │ ├── AboutView.kt
│ │ │ ├── ConferenceSelector.kt
│ │ │ ├── PlatformSpecificSettings.kt
│ │ │ └── SettingsView.kt
│ │ ├── sponsors
│ │ │ ├── SponsorDetailView.kt
│ │ │ └── SponsorsView.kt
│ │ ├── theme
│ │ │ ├── Colors.kt
│ │ │ ├── Dimensions.kt
│ │ │ ├── Theme.kt
│ │ │ ├── Type.kt
│ │ │ └── Typography.kt
│ │ ├── util
│ │ │ ├── Dialog.kt
│ │ │ ├── Image.kt
│ │ │ ├── LocalImage.kt
│ │ │ ├── NavigationBackPressWrapper.kt
│ │ │ ├── ObserveAsState.kt
│ │ │ └── WebLinkText.kt
│ │ └── venue
│ │ │ └── VenueView.kt
│ │ ├── util
│ │ ├── LocalDateTime+startOfMinute.kt
│ │ └── NavigationController.kt
│ │ └── viewmodel
│ │ ├── ApplicationViewModel.kt
│ │ ├── FeedbackDialogViewModel.kt
│ │ ├── WaitForLoadedContextModel.kt
│ │ ├── session
│ │ ├── AgendaViewModel.kt
│ │ ├── BaseSessionListViewModel.kt
│ │ ├── ScheduleViewModel.kt
│ │ ├── SessionBlockViewModel.kt
│ │ ├── SessionDayViewModel.kt
│ │ ├── SessionDetailScrollStateStorage.kt
│ │ ├── SessionDetailViewModel.kt
│ │ ├── SessionListItemViewModel.kt
│ │ ├── SpeakerDetailViewModel.kt
│ │ └── SpeakerListItemViewModel.kt
│ │ ├── settings
│ │ ├── AboutItemViewModel.kt
│ │ ├── AboutViewModel.kt
│ │ └── SettingsViewModel.kt
│ │ └── sponsor
│ │ ├── SponsorDetailViewModel.kt
│ │ ├── SponsorGroupItemViewModel.kt
│ │ ├── SponsorGroupViewModel.kt
│ │ └── SponsorListViewModel.kt
│ └── iosMain
│ └── kotlin
│ └── co
│ └── touchlab
│ └── droidcon
│ └── ui
│ ├── ComposeRootController.kt
│ ├── settings
│ └── PlatformSpecificSettings.kt
│ ├── theme
│ └── Type.ios.kt
│ └── util
│ ├── Dialog.kt
│ ├── LocalImage.kt
│ ├── NavigationBackPressWrapper.kt
│ └── ToSkiaImage.kt
├── shared
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ └── co
│ │ │ └── touchlab
│ │ │ └── droidcon
│ │ │ ├── Koin.android.kt
│ │ │ ├── domain
│ │ │ └── repository
│ │ │ │ └── impl
│ │ │ │ └── SqlDelightDriverFactory.android.kt
│ │ │ ├── service
│ │ │ ├── AndroidNotificationService.kt
│ │ │ ├── NotificationPublisher.kt
│ │ │ └── NotificationRescheduler.kt
│ │ │ └── util
│ │ │ ├── AssetResourceReader.kt
│ │ │ ├── ClasspathResourceReader.kt
│ │ │ ├── IdentifiableIntent.kt
│ │ │ ├── Platform.android.kt
│ │ │ └── formatter
│ │ │ └── AndroidDateFormatter.kt
│ └── res
│ │ ├── drawable
│ │ └── ic_baseline_insert_invitation_24.xml
│ │ └── values
│ │ └── strings.xml
│ ├── commonMain
│ ├── kotlin
│ │ └── co
│ │ │ └── touchlab
│ │ │ └── droidcon
│ │ │ ├── Koin.kt
│ │ │ ├── application
│ │ │ ├── composite
│ │ │ │ ├── AboutItem.kt
│ │ │ │ └── Settings.kt
│ │ │ ├── gateway
│ │ │ │ ├── SettingsGateway.kt
│ │ │ │ └── impl
│ │ │ │ │ └── DefaultSettingsGateway.kt
│ │ │ ├── repository
│ │ │ │ ├── AboutRepository.kt
│ │ │ │ ├── SettingsRepository.kt
│ │ │ │ └── impl
│ │ │ │ │ ├── DefaultAboutRepository.kt
│ │ │ │ │ └── DefaultSettingsRepository.kt
│ │ │ └── service
│ │ │ │ ├── Notification.kt
│ │ │ │ ├── NotificationSchedulingService.kt
│ │ │ │ ├── NotificationService.kt
│ │ │ │ └── impl
│ │ │ │ └── DefaultNotificationSchedulingService.kt
│ │ │ ├── composite
│ │ │ └── Url.kt
│ │ │ ├── domain
│ │ │ ├── composite
│ │ │ │ ├── ScheduleItem.kt
│ │ │ │ └── SponsorGroupWithSponsors.kt
│ │ │ ├── entity
│ │ │ │ ├── Conference.kt
│ │ │ │ ├── DomainEntity.kt
│ │ │ │ ├── Profile.kt
│ │ │ │ ├── Room.kt
│ │ │ │ ├── Session.kt
│ │ │ │ ├── Sponsor.kt
│ │ │ │ └── SponsorGroup.kt
│ │ │ ├── gateway
│ │ │ │ ├── SessionGateway.kt
│ │ │ │ ├── SponsorGateway.kt
│ │ │ │ └── impl
│ │ │ │ │ ├── DefaultSessionGateway.kt
│ │ │ │ │ └── DefaultSponsorGateway.kt
│ │ │ ├── repository
│ │ │ │ ├── ConferenceRepository.kt
│ │ │ │ ├── ProfileRepository.kt
│ │ │ │ ├── Repository.kt
│ │ │ │ ├── RoomRepository.kt
│ │ │ │ ├── SessionRepository.kt
│ │ │ │ ├── SponsorGroupRepository.kt
│ │ │ │ ├── SponsorRepository.kt
│ │ │ │ └── impl
│ │ │ │ │ ├── BaseRepository.kt
│ │ │ │ │ ├── SqlDelightConferenceRepository.kt
│ │ │ │ │ ├── SqlDelightDriverFactory.kt
│ │ │ │ │ ├── SqlDelightProfileRepository.kt
│ │ │ │ │ ├── SqlDelightRoomRepository.kt
│ │ │ │ │ ├── SqlDelightSessionRepository.kt
│ │ │ │ │ ├── SqlDelightSponsorGroupRepository.kt
│ │ │ │ │ ├── SqlDelightSponsorRepository.kt
│ │ │ │ │ └── adapter
│ │ │ │ │ └── InstantSqlDelightAdapter.kt
│ │ │ └── service
│ │ │ │ ├── AnalyticsService.kt
│ │ │ │ ├── ConferenceConfigProvider.kt
│ │ │ │ ├── DateTimeService.kt
│ │ │ │ ├── FeedbackService.kt
│ │ │ │ ├── ScheduleService.kt
│ │ │ │ ├── ServerApi.kt
│ │ │ │ ├── SyncService.kt
│ │ │ │ ├── UserIdProvider.kt
│ │ │ │ └── impl
│ │ │ │ ├── DefaultApiDataSource.kt
│ │ │ │ ├── DefaultConferenceConfigProvider.kt
│ │ │ │ ├── DefaultDateTimeService.kt
│ │ │ │ ├── DefaultFeedbackService.kt
│ │ │ │ ├── DefaultScheduleService.kt
│ │ │ │ ├── DefaultServerApi.kt
│ │ │ │ ├── DefaultSyncService.kt
│ │ │ │ ├── DefaultUserIdProvider.kt
│ │ │ │ ├── ResourceReader.kt
│ │ │ │ ├── dto
│ │ │ │ ├── AboutDto.kt
│ │ │ │ ├── ConferencesDto.kt
│ │ │ │ ├── ScheduleDto.kt
│ │ │ │ ├── SpeakersDto.kt
│ │ │ │ ├── SponsorSessionsDto.kt
│ │ │ │ └── SponsorsDto.kt
│ │ │ │ └── json
│ │ │ │ ├── AboutJsonResourceDataSource.kt
│ │ │ │ └── JsonResourceReader.kt
│ │ │ ├── dto
│ │ │ └── WebLink.kt
│ │ │ ├── service
│ │ │ ├── DeepLinkNotificationHandler.kt
│ │ │ └── ParseUrlViewService.kt
│ │ │ └── util
│ │ │ ├── AppChecker.kt
│ │ │ ├── Platform.kt
│ │ │ └── formatter
│ │ │ └── DateFormatter.kt
│ ├── resources
│ │ └── about.json
│ └── sqldelight
│ │ └── co
│ │ └── touchlab
│ │ └── droidcon
│ │ └── db
│ │ ├── Conference.sq
│ │ ├── Profile.sq
│ │ ├── Room.sq
│ │ ├── Session.sq
│ │ ├── SessionSpeaker.sq
│ │ ├── Sponsor.sq
│ │ ├── SponsorGroup.sq
│ │ └── SponsorRepresentative.sq
│ └── iosMain
│ └── kotlin
│ └── co
│ └── touchlab
│ └── droidcon
│ ├── Koin.ios.kt
│ ├── MainScope.kt
│ ├── domain
│ └── repository
│ │ └── impl
│ │ └── SqlDelightDriverFactory.ios.kt
│ ├── service
│ └── IOSNotificationService.kt
│ └── util
│ ├── AppInit.kt
│ ├── BundleResourceReader.kt
│ ├── Platform.ios.kt
│ └── WrapMultiThreadCallback.kt
├── tlsmall.png
├── updateDates.py
└── venuemaps
├── dcldn-2024-venue-map-1.jpg
└── dcnyc-2024-venue-map-1.jpg
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.{kt,kts}]
4 | end_of_line = lf
5 | ij_kotlin_packages_to_use_import_on_demand = true
6 | ij_kotlin_allow_trailing_comma = true
7 | ij_kotlin_allow_trailing_comma_on_call_site = true
8 | ij_kotlin_imports_layout = *
9 | ij_kotlin_indent_before_arrow_on_new_line = false
10 | ij_kotlin_line_break_after_multiline_when_entry = true
11 | indent_size = 4
12 | indent_style = space
13 | insert_final_newline = true
14 | parameter-list-wrapping = true
15 | ktlint_argument_list_wrapping_ignore_when_parameter_count_greater_or_equal_than = 8
16 | ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 4
17 | ktlint_code_style = android_studio
18 | ktlint_enum_entry_name_casing = upper_or_camel_cases
19 | ktlint_function_naming_ignore_when_annotated_with = Composable
20 | ktlint_function_signature_body_expression_wrapping = default
21 | ktlint_ignore_back_ticked_identifier = false
22 | max_line_length = 140
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | pull_request:
4 | push:
5 | branches: [ main ] # Trigger on pushes to the main branch
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build:
10 | runs-on: macos-latest
11 | steps:
12 | - name: Checkout source code
13 | uses: actions/checkout@v4
14 |
15 | - name: Set up JDK 17
16 | uses: actions/setup-java@v4
17 | with:
18 | java-version: '17'
19 | distribution: 'corretto'
20 |
21 | - name: Setup Gradle
22 | uses: gradle/actions/setup-gradle@v4
23 |
24 | - name: Setup Android SDK
25 | uses: android-actions/setup-android@v3
26 |
27 | - name: Check, Assemble Android and compile iOS
28 | run: ./gradlew ktlintCheck assembleDebug compileKotlinIosX64 --no-daemon
--------------------------------------------------------------------------------
/.github/workflows/gradle-wrapper.yaml:
--------------------------------------------------------------------------------
1 | name: gradle-wrapper
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - 'gradlew'
7 | - 'gradlew.bat'
8 | - 'gradle/wrapper/**'
9 |
10 | jobs:
11 | validate:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: gradle/actions/wrapper-validation@v4
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # .gitignore Cleanup
2 |
3 | # Built application files
4 | *.apk
5 | *.ap_
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | build/
18 | captures/
19 | .cxx
20 | .externalNativeBuild
21 |
22 | # Gradle files
23 | .gradle/
24 | /local.properties
25 | local.properties
26 |
27 | # IDE files
28 | ## Android Studio
29 | *.iws
30 | *.ipr
31 | *.iml
32 | .idea/
33 | .navigation/
34 |
35 | ## Eclipse
36 | proguard/
37 | .classpath
38 | .project
39 |
40 | ## iOS/Mac
41 | .DS_Store
42 | timeline.xctimeline
43 | playground.xcworkspace
44 | *.xcworkspace
45 | Pods/
46 |
47 | # Keystore files
48 | *.jks
49 |
50 | # Log Files
51 | *.log
52 |
53 | # Fastlane
54 | fastlane/report.xml
55 | fastlane/Preview.html
56 | fastlane/screenshots
57 | fastlane/test_output
58 |
59 | # Project-specific files
60 | buildboth.sh
61 | google-services.json
62 | GoogleService-Info.plist
63 | .kotlin
64 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # DroidconKotlin Development Guide
2 |
3 | ## Build Commands
4 | - Build: `./gradlew build`
5 | - Clean build: `./gradlew clean build`
6 | - Check (includes lint): `./gradlew check`
7 | - Android lint: `./gradlew lint`
8 | - Run tests: `./gradlew test`
9 | - ktlint check: `./gradlew ktlintCheck`
10 | - ktlint format: `./gradlew ktlintFormat`
11 | - Build ios: `cd /Users/kevingalligan/devel/DroidconKotlin/ios/Droidcon && xcodebuild -scheme Droidcon -sdk iphonesimulator`
12 |
13 | ## Modules
14 |
15 | - android: The Android app
16 | - ios: The iOS app
17 | - shared: Shared logic code
18 | - shared-ui: UI implemented with Compose Multiplatform and used by both Android and iOS
19 |
20 | ## Libraries
21 |
22 | - Hyperdrive: KMP-focused architecture library. It is open source but rarely used by other apps. See docs/HyperDrivev1.md
23 |
24 | ## Code Style
25 | - Kotlin Multiplatform project (Android/iOS)
26 | - Use ktlint for formatting (version 1.4.0)
27 | - Follow dependency injection pattern with Koin
28 | - Repository pattern for data access
29 | - Compose UI for shared UI components
30 | - Class/function names: PascalCase for classes, camelCase for functions
31 | - Interface implementations: Prefix with `Default` (e.g., `DefaultRepository`)
32 | - Organize imports by package, no wildcard imports
33 | - Type-safe code with explicit type declarations
34 | - Coroutines for asynchronous operations
35 | - Proper error handling with try/catch blocks
36 |
37 | ## Claude Document Formats and Instructions
38 | See APISummaryFormat.md and StructuredInstructionFormats.md
39 |
40 | ## Architecture Notes
41 | - App startup logic is handled in `co.touchlab.droidcon.viewmodel.ApplicationViewModel`
42 |
43 | ## Current Task
44 |
45 | Cleaning up the app and prepping for release
--------------------------------------------------------------------------------
/IOSDEV.md:
--------------------------------------------------------------------------------
1 | # Minimal iOS Dev Instructions
2 |
3 | The absolute bare minimum install instructions, for an iOS dev who maybe hasn't done any Android
4 |
5 |
6 | 1 - Install the Android SDK
7 |
8 | 2 - Go here and download the .zip file for Mac under “Command Line Tools Only”
9 | On the command line, run the following commands:
10 | ```sdkmanager “platform-tools”
11 | sdkmanager “platforms;android-28”
12 | cd ~/Library/Android/sdk
13 | export ANDROID_HOME=~/Library/Android/sdk
14 | export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platorm-tools
15 | source ~/.bash_profile
16 | ```
17 | 3 - Download IntelliJ Community: https://www.jetbrains.com/idea/download/#section=mac
18 |
19 | 4 - `git clone https://github.com/touchlab/DroidconKotlin`
20 |
21 | 5 - Go to the “Terminal” tab on the bottom and run: `./gradlew build`
22 |
23 | 6 - Right click on “DroidconKotlin” in the “Project” tab on the left and select “Reveal in Finder”
24 |
25 | 7 - Go to "/iosApp/” and open “iosApp.xcworkspace”
26 |
27 | 8 - Run the app!
28 |
--------------------------------------------------------------------------------
/MEDIA.md:
--------------------------------------------------------------------------------
1 | # Media
2 |
3 | A sample of blog posts and videos about the application project.
4 |
5 | [Touchlab Blog - Droidcon NYC iOS app with Compose](https://touchlab.co/droidcon-nyc-ios-app-with-compose/)
6 |
7 | [Touchlab Blog - Compose UI for iOS](https://touchlab.co/compose-ui-for-ios/)
8 |
9 | [Medium - Droidcon NYC App!](https://medium.com/@kpgalligan/droidcon-nyc-app-da868bdef387)
10 |
11 | [Medium - Kotlin Multiplatform in the App Store!](https://medium.com/@kpgalligan/kotlin-multiplatform-in-the-app-store-c3a50c24f93b)
12 |
13 | [Youtube - Droidcon SF](https://www.youtube.com/watch?v=c8IkWGmlcNE)
14 |
15 | [Youtube - Kotlinconf](https://www.youtube.com/watch?v=Dul17VSiejo)
16 |
17 | [Youtube - Kotlin Multiplatform @ Android Summit](https://www.youtube.com/watch?v=oeREzhXx7uw)
18 |
19 | [Youtube - Droidcon App Kotlin Multiplatform](https://www.youtube.com/watch?v=YAeDK3Ei0Lk&feature=youtu.be)
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Droidcon Mobile Clients
2 |
3 | ## General Info
4 |
5 | This project has a pair of native mobile applications for Global Droidcon events. It is built with Compose Multiplatform to run on both Android and iOS.
6 |
7 | > ## Subscribe!
8 | >
9 | > We build solutions that get teams started smoothly with Kotlin Multiplatform and ensure their success in production. Join our community to learn how your peers are adopting KMP.
10 | [Sign up here](https://touchlab.co/?s=shownewsletter)!
11 |
12 | ## Building
13 |
14 | The apps need a Firebase account set up to run. You'll need to get the `google-services.json` and put it in `android/google-services.json` for Android, and
15 | the `GoogleService-Info.plist` and put that in `ios/Droidcon/Droidcon/GoogleService-Info.plist` for iOS.
16 |
17 | ## Compose UI for both!
18 |
19 | This app has come a long way! It was one of the earliest KMP apps in the iOS app store, and was certainly the first Compose Multiplatform app in the app store, as it was built and released before there was technically a tech preview.
20 |
21 | [Check out the blog post](https://touchlab.co/droidcon-nyc-ios-app-with-compose/)
22 |
23 | CMP has come a long way. Back in 2022, the Compose experience on iOS wasn't great. We kept a SwiftUI version of the UI as the main UI for the iOS app, and let you turn on CMP as an early preview. As of March 2025, the SwiftUI version has been removed entirely. Why do the extra work? But, if you want to do a side-by-side, grab a version from main through Feb 2025 and check it out. Prior versions were built and released for each conference, but this updated version is designed to be used for all Droidcon events. As that was a fairly major update, we decided to drop the parallel SwiftUI.
24 |
25 | ## Media
26 |
27 | [Blog posts and videos ->](MEDIA.md)
28 |
29 | ## About
30 |
31 | Droidcon Mobile App brought to you by...
32 |
33 | [](https://touchlab.co)
34 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/android/mock-google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "606665771234",
4 | "project_id": "mock-firebase-project",
5 | "storage_bucket": "mock-firebase-project.appspot.com"
6 | },
7 | "client": [
8 | {
9 | "client_info": {
10 | "mobilesdk_app_id": "1:606665771229:android:c1f0f09aa42abc12",
11 | "android_client_info": {
12 | "package_name": "co.touchlab.droidcon.london"
13 | }
14 | },
15 | "oauth_client": [
16 | {
17 | "client_id": "123455771229-sc9bpuefbjceq7i1qabk7gssstefrdlv.apps.googleusercontent.com",
18 | "client_type": 1,
19 | "android_info": {
20 | "package_name": "co.touchlab.droidcon.london",
21 | "certificate_hash": "7f254b538565cfe6c28a88744985f015b1534980"
22 | }
23 | },
24 | {
25 | "client_id": "123455771229-sc9bpuefbjceq7i1qabk7gssstefrdlv.apps.googleusercontent.com",
26 | "client_type": 3
27 | }
28 | ],
29 | "api_key": [
30 | {
31 | "current_key": "AIzaSyCNzhU2a9gMc_JHurHbywOrRI9Vj4VQZZZ"
32 | }
33 | ],
34 | "services": {
35 | "appinvite_service": {
36 | "other_platform_oauth_client": [
37 | {
38 | "client_id": "413392989754-04u9tv32474rj0pmbfksirt4ti02a64r.apps.googleusercontent.com",
39 | "client_type": 3
40 | }
41 | ]
42 | }
43 | }
44 | }
45 | ],
46 | "configuration_version": "1"
47 | }
--------------------------------------------------------------------------------
/android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/android/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/android/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.android
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.content.Context
6 | import android.content.SharedPreferences
7 | import co.touchlab.droidcon.android.service.impl.AndroidAnalyticsService
8 | import co.touchlab.droidcon.android.service.impl.DefaultParseUrlViewService
9 | import co.touchlab.droidcon.android.util.NotificationLocalizedStringFactory
10 | import co.touchlab.droidcon.application.service.NotificationSchedulingService
11 | import co.touchlab.droidcon.domain.service.AnalyticsService
12 | import co.touchlab.droidcon.domain.service.impl.ResourceReader
13 | import co.touchlab.droidcon.initKoin
14 | import co.touchlab.droidcon.service.ParseUrlViewService
15 | import co.touchlab.droidcon.ui.uiModule
16 | import co.touchlab.droidcon.util.ClasspathResourceReader
17 | import com.google.firebase.analytics.ktx.analytics
18 | import com.google.firebase.ktx.Firebase
19 | import com.russhwolf.settings.ExperimentalSettingsApi
20 | import com.russhwolf.settings.ObservableSettings
21 | import com.russhwolf.settings.SharedPreferencesSettings
22 | import kotlinx.coroutines.CoroutineScope
23 | import kotlinx.coroutines.Dispatchers
24 | import kotlinx.coroutines.SupervisorJob
25 | import org.koin.core.component.KoinComponent
26 | import org.koin.dsl.module
27 |
28 | @OptIn(ExperimentalSettingsApi::class)
29 | class MainApp :
30 | Application(),
31 | KoinComponent {
32 | private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
33 |
34 | override fun onCreate() {
35 | super.onCreate()
36 | initKoin(
37 | module {
38 | single { this@MainApp }
39 | single> { MainActivity::class.java }
40 | single {
41 | get().getSharedPreferences("DROIDCON_SETTINGS_2024", Context.MODE_PRIVATE)
42 | }
43 | single { SharedPreferencesSettings(delegate = get()) }
44 |
45 | single {
46 | DefaultParseUrlViewService()
47 | }
48 |
49 | single {
50 | ClasspathResourceReader()
51 | }
52 |
53 | single {
54 | NotificationLocalizedStringFactory(context = get())
55 | }
56 |
57 | single {
58 | AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics)
59 | }
60 | } + uiModule,
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidAnalyticsService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.android.service.impl
2 |
3 | import android.os.Bundle
4 | import co.touchlab.droidcon.domain.service.AnalyticsService
5 | import com.google.firebase.analytics.FirebaseAnalytics
6 |
7 | class AndroidAnalyticsService(private val firebaseAnalytics: FirebaseAnalytics) : AnalyticsService {
8 |
9 | override fun logEvent(name: String, params: Map) {
10 | val bundle = Bundle()
11 | params.keys.forEach { key ->
12 | when (val obj = params[key]) {
13 | is String -> bundle.putString(key, obj)
14 | is Boolean -> bundle.putBoolean(key, obj)
15 | is Int -> bundle.putInt(key, obj)
16 | is Long -> bundle.putLong(key, obj)
17 | is Double -> bundle.putDouble(key, obj)
18 | is BooleanArray -> bundle.putBooleanArray(key, obj)
19 | is IntArray -> bundle.putIntArray(key, obj)
20 | is LongArray -> bundle.putLongArray(key, obj)
21 | is DoubleArray -> bundle.putDoubleArray(key, obj)
22 | else -> throw IllegalArgumentException("Unsupported type $obj with key $key")
23 | }
24 | }
25 | firebaseAnalytics.logEvent(name, bundle)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/android/src/main/java/co/touchlab/droidcon/android/service/impl/DefaultFirebaseMessagingService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.android.service.impl
2 |
3 | import android.app.NotificationManager
4 | import androidx.core.app.NotificationCompat
5 | import androidx.core.content.getSystemService
6 | import co.touchlab.droidcon.application.service.Notification
7 | import co.touchlab.droidcon.service.AndroidNotificationService
8 | import com.google.firebase.messaging.FirebaseMessagingService
9 | import com.google.firebase.messaging.RemoteMessage
10 | import kotlinx.coroutines.MainScope
11 | import kotlinx.coroutines.launch
12 | import org.koin.android.ext.android.inject
13 |
14 | class DefaultFirebaseMessagingService : FirebaseMessagingService() {
15 | private val notificationService: AndroidNotificationService by inject()
16 |
17 | override fun onNewToken(token: String) {
18 | super.onNewToken(token)
19 | }
20 |
21 | override fun onMessageReceived(message: RemoteMessage) {
22 | super.onMessageReceived(message)
23 |
24 | if (message.data.isNotEmpty() && message.data[Notification.Keys.NOTIFICATION_TYPE] == Notification.Values.REFRESH_DATA_TYPE) {
25 | MainScope().launch {
26 | notificationService.handleNotification(
27 | Notification.Remote.RefreshData,
28 | )
29 | }
30 | }
31 |
32 | // If we have notification, we're running in foreground and should show it ourselves.
33 | val originalNotification = message.notification ?: return
34 | val notification = NotificationCompat.Builder(this, message.notification?.channelId ?: "")
35 | .setContentTitle(originalNotification.title)
36 | .setContentText(originalNotification.body)
37 | .apply {
38 | originalNotification.channelId?.let {
39 | setChannelId(it)
40 | }
41 | }
42 | .build()
43 |
44 | val notificationManager = getSystemService()
45 | notificationManager?.notify(0, notification)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/android/src/main/java/co/touchlab/droidcon/android/service/impl/DefaultParseUrlViewService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.android.service.impl
2 |
3 | import android.util.Patterns
4 | import co.touchlab.droidcon.dto.WebLink
5 | import co.touchlab.droidcon.service.ParseUrlViewService
6 |
7 | class DefaultParseUrlViewService : ParseUrlViewService {
8 |
9 | private val urlRegex = Patterns.WEB_URL.toRegex()
10 |
11 | override fun parse(text: String): List = urlRegex.findAll(text).map { WebLink(it.range, it.value) }.toList()
12 | }
13 |
--------------------------------------------------------------------------------
/android/src/main/java/co/touchlab/droidcon/android/util/NotificationLocalizedStringFactory.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.android.util
2 |
3 | import android.content.Context
4 | import co.touchlab.droidcon.R
5 | import co.touchlab.droidcon.application.service.NotificationSchedulingService
6 |
7 | class NotificationLocalizedStringFactory(private val context: Context) : NotificationSchedulingService.LocalizedStringFactory {
8 |
9 | override fun reminderTitle(roomName: String?): String {
10 | val ending = roomName?.let { context.getString(R.string.notification_reminder_title_in_room, it) } ?: ""
11 | return context.getString(R.string.notification_reminder_title_base, ending)
12 | }
13 |
14 | override fun reminderBody(sessionTitle: String): String = context.getString(R.string.notification_reminder_body, sessionTitle)
15 |
16 | override fun feedbackTitle(): String = context.getString(R.string.notification_feedback_title)
17 |
18 | override fun feedbackBody(): String = context.getString(R.string.notification_feedback_body)
19 | }
20 |
--------------------------------------------------------------------------------
/android/src/main/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable-nodpi/about_touchlab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/android/src/main/res/drawable-nodpi/about_touchlab.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable/about_droidcon.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
30 |
33 |
34 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/ic_baseline_insert_invitation_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/linkedin.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/twitter.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF202020
4 |
5 |
--------------------------------------------------------------------------------
/android/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFffffff
4 | #FF7de1c3
5 |
6 |
--------------------------------------------------------------------------------
/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Droidcon
3 |
4 | Upcoming event%s
5 | " in %s"
6 | %s is starting soon.
7 | Feedback Time!
8 | Your Feedback is Requested.
9 |
10 | Droidcon London 2024
11 |
12 |
13 |
--------------------------------------------------------------------------------
/android/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 |
3 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
4 | plugins {
5 | alias(libs.plugins.crashlytics).apply(false)
6 | alias(libs.plugins.googleServices).apply(false)
7 | alias(libs.plugins.kotlinMultiplatform).apply(false)
8 | alias(libs.plugins.androidLibrary).apply(false)
9 | alias(libs.plugins.sqlDelight).apply(false)
10 | alias(libs.plugins.jetbrainsCompose).apply(false)
11 | alias(libs.plugins.composeCompiler).apply(false)
12 | alias(libs.plugins.ktlint)
13 | alias(libs.plugins.serialization).apply(false)
14 | alias(libs.plugins.skie).apply(false)
15 | }
16 |
17 | subprojects {
18 | apply(plugin = rootProject.libs.plugins.ktlint.get().pluginId)
19 |
20 | ktlint {
21 | version.set("0.37.2")
22 | enableExperimentalRules.set(true)
23 | verbose.set(true)
24 | filter {
25 | exclude { it.file.path.contains("build/") }
26 | }
27 | }
28 |
29 | afterEvaluate {
30 | tasks.named("check") {
31 | dependsOn(tasks.getByName("ktlintCheck"))
32 | }
33 | }
34 |
35 | tasks.withType(KotlinCompile::class).all {
36 | kotlinOptions {
37 | jvmTarget = "1.8"
38 | }
39 | }
40 | configure {
41 | version.set("1.4.0")
42 | }
43 | }
44 |
45 | tasks.register("clean") {
46 | delete(rootProject.layout.buildDirectory)
47 | }
48 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | android.injected.testOnly=false
21 | org.jetbrains.compose.experimental.uikit.enabled=true
22 | org.gradle.caching=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/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.12-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/ios/Droidcon/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/xcode
2 | # Edit at https://www.gitignore.io/?templates=xcode
3 |
4 | ### Xcode ###
5 | # Xcode
6 | #
7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
8 |
9 | ## User settings
10 | xcuserdata/
11 |
12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
13 | *.xcscmblueprint
14 | *.xccheckout
15 |
16 | ### Xcode Patch ###
17 | **/xcshareddata/WorkspaceSettings.xcsettings
18 |
19 | # End of https://www.gitignore.io/api/xcode
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Accent.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xE6",
9 | "green" : "0x14",
10 | "red" : "0x00"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xE6",
27 | "green" : "0x14",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "dcLondon24-Icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/AppIcon.appiconset/dcLondon24-Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/AppIcon.appiconset/dcLondon24-Icon.png
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/AttendButton.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xE6",
9 | "green" : "0x14",
10 | "red" : "0x00"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xE6",
27 | "green" : "0x14",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/AttendButton_Foreground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "extended-gray",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "white" : "1.000"
9 | }
10 | },
11 | "idiom" : "universal"
12 | },
13 | {
14 | "appearances" : [
15 | {
16 | "appearance" : "luminosity",
17 | "value" : "dark"
18 | }
19 | ],
20 | "color" : {
21 | "color-space" : "extended-gray",
22 | "components" : {
23 | "alpha" : "1.000",
24 | "white" : "1.000"
25 | }
26 | },
27 | "idiom" : "universal"
28 | }
29 | ],
30 | "info" : {
31 | "author" : "xcode",
32 | "version" : 1
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Attending/AttendingConflict.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x2C",
9 | "green" : "0x58",
10 | "red" : "0xF1"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x2C",
27 | "green" : "0x58",
28 | "red" : "0xF1"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Attending/AttendingNormal.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xFD",
9 | "green" : "0xD1",
10 | "red" : "0x68"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xFD",
27 | "green" : "0xD1",
28 | "red" : "0x68"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Attending/AttendingPast.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x66",
9 | "green" : "0x66",
10 | "red" : "0x66"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xA1",
27 | "green" : "0xA1",
28 | "red" : "0xA1"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Attending/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Divider.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "platform" : "ios",
6 | "reference" : "systemGray2Color"
7 | },
8 | "idiom" : "universal"
9 | },
10 | {
11 | "appearances" : [
12 | {
13 | "appearance" : "luminosity",
14 | "value" : "dark"
15 | }
16 | ],
17 | "color" : {
18 | "platform" : "ios",
19 | "reference" : "systemGray2Color"
20 | },
21 | "idiom" : "universal"
22 | }
23 | ],
24 | "info" : {
25 | "author" : "xcode",
26 | "version" : 1
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/ElevatedBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xFA",
9 | "green" : "0xFA",
10 | "red" : "0xFA"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "platform" : "ios",
24 | "reference" : "systemGray4Color"
25 | },
26 | "idiom" : "universal"
27 | }
28 | ],
29 | "info" : {
30 | "author" : "xcode",
31 | "version" : 1
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/ElevatedBackgroundDisabled.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "platform" : "ios",
6 | "reference" : "systemGray5Color"
7 | },
8 | "idiom" : "universal"
9 | },
10 | {
11 | "appearances" : [
12 | {
13 | "appearance" : "luminosity",
14 | "value" : "dark"
15 | }
16 | ],
17 | "color" : {
18 | "platform" : "ios",
19 | "reference" : "systemGray2Color"
20 | },
21 | "idiom" : "universal"
22 | }
23 | ],
24 | "info" : {
25 | "author" : "xcode",
26 | "version" : 1
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/ElevatedHeaderBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xF9",
9 | "green" : "0xF9",
10 | "red" : "0xF9"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x33",
27 | "green" : "0x33",
28 | "red" : "0x33"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Dissatisfied.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "baseline_sentiment_very_dissatisfied_black_48dp.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | },
21 | "properties" : {
22 | "template-rendering-intent" : "template"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Dissatisfied.imageset/baseline_sentiment_very_dissatisfied_black_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Dissatisfied.imageset/baseline_sentiment_very_dissatisfied_black_48dp.png
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Normal.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "baseline_sentiment_satisfied_black_48dp.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | },
21 | "properties" : {
22 | "template-rendering-intent" : "template"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Normal.imageset/baseline_sentiment_satisfied_black_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Normal.imageset/baseline_sentiment_satisfied_black_48dp.png
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Satisfied.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "baseline_sentiment_satisfied_alt_black_48dp.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | },
21 | "properties" : {
22 | "template-rendering-intent" : "template"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Satisfied.imageset/baseline_sentiment_satisfied_alt_black_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/Feedback/Feedback_Satisfied.imageset/baseline_sentiment_satisfied_alt_black_48dp.png
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/LaunchScreen/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/LaunchScreen/LaunchScreen_Background.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xC3",
9 | "green" : "0xE1",
10 | "red" : "0x7D"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xC3",
27 | "green" : "0xE1",
28 | "red" : "0x7D"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/LaunchScreen/LaunchScreen_Icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "London 24-Splash screen.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_droidcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "droidcon_logo_light.pdf",
5 | "idiom" : "universal"
6 | },
7 | {
8 | "appearances" : [
9 | {
10 | "appearance" : "luminosity",
11 | "value" : "dark"
12 | }
13 | ],
14 | "filename" : "droidcon_logo_night.pdf",
15 | "idiom" : "universal"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "preserves-vector-representation" : true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_droidcon.imageset/droidcon_logo_light.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_droidcon.imageset/droidcon_logo_light.pdf
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_droidcon.imageset/droidcon_logo_night.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_droidcon.imageset/droidcon_logo_night.pdf
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_kotlin.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "small Kotlin Full Color Logo on White RGB.svg",
5 | "idiom" : "universal"
6 | },
7 | {
8 | "appearances" : [
9 | {
10 | "appearance" : "luminosity",
11 | "value" : "dark"
12 | }
13 | ],
14 | "filename" : "small Kotlin Full Color Logo on Black RGB.svg",
15 | "idiom" : "universal"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | },
22 | "properties" : {
23 | "preserves-vector-representation" : true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_touchlab.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "TL_Gradient.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | },
21 | "properties" : {
22 | "preserves-vector-representation" : true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_touchlab.imageset/TL_Gradient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/Logos/about_touchlab.imageset/TL_Gradient.png
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/linkedin.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "linked-in.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/linkedin.imageset/linked-in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/Logos/linkedin.imageset/linked-in.png
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/twitter.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "twitter.png",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Logos/twitter.imageset/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Assets.xcassets/Logos/twitter.imageset/twitter.png
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/NavBar/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/NavBar/NavBar_Background.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xC3",
9 | "green" : "0xE0",
10 | "red" : "0x7C"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xC3",
27 | "green" : "0xE0",
28 | "red" : "0x7C"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/NavBar/NavBar_Foreground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xE6",
9 | "green" : "0x14",
10 | "red" : "0x00"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xE6",
27 | "green" : "0x14",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/Shadow.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.200",
8 | "blue" : "0x00",
9 | "green" : "0x00",
10 | "red" : "0x00"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "0.000",
26 | "blue" : "0x00",
27 | "green" : "0x00",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/TabBar/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/TabBar/TabBar_Background.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xC3",
9 | "green" : "0xE0",
10 | "red" : "0x7C"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xC3",
27 | "green" : "0xE0",
28 | "red" : "0x7C"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/TabBar/TabBar_Foreground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.600",
8 | "blue" : "0xE6",
9 | "green" : "0x14",
10 | "red" : "0x00"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "0.600",
26 | "blue" : "0xE6",
27 | "green" : "0x14",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/TabBar/TabBar_Foreground_Selected.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "0.341",
10 | "red" : "0.278"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xFF",
27 | "green" : "0x57",
28 | "red" : "0x47"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Assets.xcassets/TextFieldBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xEE",
9 | "green" : "0xEE",
10 | "red" : "0xEE"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x66",
27 | "green" : "0x66",
28 | "red" : "0x66"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/ComposeController.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import DroidconKit
3 |
4 | struct ComposeController: UIViewControllerRepresentable {
5 |
6 | let viewModel: WaitForLoadedContextModel
7 |
8 | func makeUIViewController(context: Context) -> some UIViewController {
9 | getRootController(viewModel: viewModel)
10 | }
11 |
12 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
13 | }
14 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Droidcon.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/DroidconApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import DroidconKit
3 |
4 | @main
5 | struct DroidconApp: App {
6 | @UIApplicationDelegateAdaptor(AppDelegate.self)
7 | var appDelegate
8 |
9 | @StateObject
10 | private var lifecycleManager = LifecycleManager()
11 |
12 | init() {
13 | setupNavBarAppearance()
14 | setupTabBarAppearance()
15 | }
16 |
17 | var body: some Scene {
18 | WindowGroup {
19 | let viewModel = koin.waitForLoadedContextModel
20 | ComposeController(viewModel: viewModel)
21 | .ignoresSafeArea()
22 | .attach(viewModel: viewModel)
23 | .environmentObject(lifecycleManager)
24 | }
25 | }
26 |
27 | private func setupNavBarAppearance() {
28 | let appearance = UINavigationBarAppearance()
29 | appearance.configureWithDefaultBackground()
30 | appearance.backgroundColor = UIColor(named: "NavBar_Background")
31 | appearance.titleTextAttributes = UIColor(named: "NavBar_Foreground").map { [.foregroundColor: $0] } ?? [:]
32 | UINavigationBar.appearance().tintColor = UIColor(named: "NavBar_Foreground")
33 | UINavigationBar.appearance().standardAppearance = appearance
34 | UINavigationBar.appearance().compactAppearance = appearance
35 | UINavigationBar.appearance().scrollEdgeAppearance = appearance
36 | }
37 |
38 | private func setupTabBarAppearance() {
39 | let itemAppearance = UITabBarItemAppearance()
40 | itemAppearance.configureWithDefault(for: .inline)
41 | itemAppearance.normal.iconColor = UIColor(named: "TabBar_Foreground")
42 | itemAppearance.normal.titleTextAttributes = UIColor(named: "TabBar_Foreground").map { [.foregroundColor: $0] } ?? [:]
43 | itemAppearance.selected.iconColor = UIColor(named: "TabBar_Foreground_Selected")
44 | itemAppearance.selected.titleTextAttributes = UIColor(named: "TabBar_Foreground_Selected").map { [.foregroundColor: $0] } ?? [:]
45 | let appearance = UITabBarAppearance()
46 | appearance.configureWithDefaultBackground()
47 | appearance.backgroundColor = UIColor(named: "TabBar_Background")
48 | appearance.inlineLayoutAppearance = itemAppearance
49 | appearance.compactInlineLayoutAppearance = itemAppearance
50 | appearance.stackedLayoutAppearance = itemAppearance
51 |
52 | UITabBar.appearance().standardAppearance = appearance
53 | if #available(iOS 15.0, *) {
54 | UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance(barAppearance: appearance)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/IOSAnalyticsService.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Firebase
3 | import DroidconKit
4 |
5 | final class IOSAnalyticsService: AnalyticsService {
6 | func logEvent(name: String, params: [String: Any]) {
7 | Analytics.logEvent(name, parameters: params)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSApplicationQueriesSchemes
6 |
7 | fluttercon
8 |
9 | CADisableMinimumFrameDurationOnPhone
10 |
11 | CFBundleDevelopmentRegion
12 | $(DEVELOPMENT_LANGUAGE)
13 | CFBundleExecutable
14 | $(EXECUTABLE_NAME)
15 | CFBundleIdentifier
16 | $(PRODUCT_BUNDLE_IDENTIFIER)
17 | CFBundleInfoDictionaryVersion
18 | 6.0
19 | CFBundleName
20 | Droidcon London 2024
21 | CFBundlePackageType
22 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
23 | CFBundleShortVersionString
24 | $(MARKETING_VERSION)
25 | CFBundleURLTypes
26 |
27 |
28 | CFBundleTypeRole
29 | Editor
30 | CFBundleURLName
31 | droidconlondon
32 | CFBundleURLSchemes
33 |
34 | droidconlondon
35 |
36 |
37 |
38 | CFBundleVersion
39 | $(CURRENT_PROJECT_VERSION)
40 | FirebaseAppDelegateProxyEnabled
41 |
42 | LSRequiresIPhoneOS
43 |
44 | UIBackgroundModes
45 |
46 | fetch
47 | remote-notification
48 |
49 | UILaunchStoryboardName
50 | LaunchScreen
51 | UIRequiredDeviceCapabilities
52 |
53 | armv7
54 |
55 | UIStatusBarStyle
56 | UIStatusBarStyleDefault
57 | UISupportedInterfaceOrientations
58 |
59 | UIInterfaceOrientationPortrait
60 |
61 | UISupportedInterfaceOrientations~ipad
62 |
63 | UIInterfaceOrientationPortrait
64 | UIInterfaceOrientationPortraitUpsideDown
65 | UIInterfaceOrientationLandscapeLeft
66 | UIInterfaceOrientationLandscapeRight
67 |
68 | UIViewControllerBasedStatusBarAppearance
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Koin.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import DroidconKit
3 |
4 | func startKoin() {
5 | let userDefaults = UserDefaults(suiteName: "DROIDCON2024_SETTINGS")!
6 |
7 | let koinApplication = DependencyInjectionKt.doInitKoinIos(userDefaults: userDefaults, analyticsService: IOSAnalyticsService())
8 | _koin = koinApplication.koin
9 | }
10 |
11 | private var _koin: Koin_coreKoin? = nil
12 | var koin: Koin_coreKoin {
13 | return _koin!
14 | }
15 |
16 | extension Koin_coreKoin {
17 | func get(_ type: T.Type = T.self, qualifier: Koin_coreQualifier? = nil, parameters: Any...) -> T {
18 | return getAny(
19 | objCObject: type,
20 | qualifier: qualifier,
21 | parameters: parameters.isEmpty ? nil : {
22 | Koin_coreParametersHolder(_values: NSMutableArray(array: parameters), useIndexedValues: KotlinBoolean(bool: false))
23 | }
24 | ) as! T
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Settings.bundle/Root.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | StringsTable
6 | Root
7 | PreferenceSpecifiers
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Settings.bundle/en.lproj/Root.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/ios/Droidcon/Droidcon/Settings.bundle/en.lproj/Root.strings
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/Utils/LifecycleManager.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import DroidconKit
3 |
4 | class LifecycleManager: SwiftUI.ObservableObject {
5 |
6 | var managedViewModel: BaseViewModel? {
7 | willSet {
8 | if let managedViewModel = managedViewModel {
9 | Logger.companion.d { [root] in "Detaching VM: \(managedViewModel.lifecycle) from \(root)" }
10 | managedViewModel.lifecycle.removeFromParent()
11 | }
12 | }
13 | didSet {
14 | if let managedViewModel = managedViewModel {
15 | Logger.companion.d { [root] in "Attaching VM: \(managedViewModel.lifecycle) to \(root)" }
16 | root.addChild(child: managedViewModel.lifecycle)
17 | }
18 | }
19 | }
20 |
21 | private let root = LifecycleGraph.Root(owner: "LifecycleManager")
22 | private let cancelAttach: CancellationToken
23 |
24 | init() {
25 | Logger.companion.i { [root] in "Initializing LifecycleManager with root: \(root)" }
26 |
27 | cancelAttach = root.attachToMainScope()
28 | }
29 |
30 | deinit {
31 | Logger.companion.i { [root] in "Destroying LifecycleManager with root: \(root)" }
32 |
33 | cancelAttach.cancel()
34 | }
35 | }
36 |
37 | struct ManagedLifecycle: ViewModifier {
38 |
39 | private let viewModel: BaseViewModel
40 |
41 | @EnvironmentObject
42 | private var lifecycleManager: LifecycleManager
43 |
44 | init(viewModel: BaseViewModel) {
45 | self.viewModel = viewModel
46 | }
47 |
48 | func body(content: Content) -> some View {
49 | content
50 | .onChange(of: viewModel) { vm in
51 | lifecycleManager.managedViewModel = vm
52 | }
53 | .onAppear {
54 | lifecycleManager.managedViewModel = viewModel
55 | }
56 | .onDisappear {
57 | lifecycleManager.managedViewModel = nil
58 | }
59 | }
60 | }
61 |
62 | extension View {
63 | func attach(viewModel: BaseViewModel) -> some View {
64 | self.modifier(ManagedLifecycle(viewModel: viewModel))
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/ios/Droidcon/Droidcon/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | "Shrug" = "¯\\_(ツ)_/¯";
2 |
3 | "Notification.Reminder.Title.Base" = "Upcoming event%@";
4 | "Notification.Reminder.Title.InRoom" = " in %@";
5 | "Notification.Reminder.Body" = "%@ is starting soon.";
6 | "Notification.Feedback.Title" = "Feedback Time!";
7 | "Notification.Feedback.Body" = "Your Feedback is Requested.";
8 |
9 | "Schedule.Title" = "Droidcon";
10 | "Schedule.TabItem.Title" = "Schedule";
11 |
12 | "Agenda.Title" = "Agenda";
13 | "Agenda.TabItem.Title" = "Agenda";
14 |
15 | "Session.List.Item.Speakers %@" = "by %@";
16 | "Session.List.Item.Room %@" = "in %@";
17 |
18 | "Session.Detail.Title" = "Session";
19 | "Session.Detail.State.InProgress" = "This session is happening now.";
20 | "Session.Detail.State.Ended" = "This session has already ended.";
21 | "Session.Detail.State.Conflict" = "This session conflicts with another session in your schedule.";
22 | "Session.Detail.Speakers" = "Speakers";
23 | "Session.Detail.AddFeedback" = "Add feedback";
24 | "Session.Detail.ChangeFeedback" = "Change your feedback";
25 |
26 | "Speaker.Detail.Title" = "Speaker";
27 |
28 | "Sponsors.Title" = "Sponsors";
29 | "Sponsors.TabItem.Title" = "Sponsors";
30 |
31 | "Sponsor.Detail.Title" = "Sponsor";
32 | "Sponsor.Detail.Representatives" = "Representatives";
33 |
34 | "Settings.Title" = "Settings";
35 | "Settings.TabItem.Title" = "Settings";
36 | "Settings.Feedback" = "Enable feedback";
37 | "Settings.Reminders" = "Enable reminders";
38 | "Settings.Compose" = "Use compose for iOS";
39 | "Settings.About" = "About";
40 |
41 | "About.Title" = "About";
42 |
43 | "Feedback.Dialog.Title %@" = "What did you think of \"%@\"?";
44 | "Feedback.Dialog.Opinion.Placeholder" = "(Optional) Suggest improvements";
45 | "Feedback.Dialog.Submit" = "Submit";
46 | "Feedback.Dialog.CloseAndDisable" = "Close and disable feedback";
47 | "Feedback.Dialog.Skip" = "Skip feedback";
48 |
49 | "Venue.Title" = "Venue Map";
50 | "Venue.TabItem.Title" = "Venue";
51 |
--------------------------------------------------------------------------------
/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ios
2 |
3 | import co.touchlab.droidcon.application.service.NotificationSchedulingService
4 | import co.touchlab.droidcon.domain.service.AnalyticsService
5 | import co.touchlab.droidcon.domain.service.impl.ResourceReader
6 | import co.touchlab.droidcon.initKoin
7 | import co.touchlab.droidcon.ios.service.DefaultParseUrlViewService
8 | import co.touchlab.droidcon.ios.util.NotificationLocalizedStringFactory
9 | import co.touchlab.droidcon.ios.util.formatter.IOSDateFormatter
10 | import co.touchlab.droidcon.service.ParseUrlViewService
11 | import co.touchlab.droidcon.ui.uiModule
12 | import co.touchlab.droidcon.util.BundleResourceReader
13 | import co.touchlab.droidcon.util.formatter.DateFormatter
14 | import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel
15 | import com.russhwolf.settings.ExperimentalSettingsApi
16 | import com.russhwolf.settings.NSUserDefaultsSettings
17 | import com.russhwolf.settings.ObservableSettings
18 | import org.koin.core.Koin
19 | import org.koin.core.KoinApplication
20 | import org.koin.dsl.module
21 | import platform.Foundation.NSBundle
22 | import platform.Foundation.NSUserDefaults
23 |
24 | @OptIn(ExperimentalSettingsApi::class)
25 | fun initKoinIos(userDefaults: NSUserDefaults, analyticsService: AnalyticsService): KoinApplication = initKoin(
26 | module {
27 | single { BundleProvider(bundle = NSBundle.mainBundle) }
28 | single { NSUserDefaultsSettings(delegate = userDefaults) }
29 | single { BundleResourceReader(bundle = get().bundle) }
30 |
31 | single { IOSDateFormatter() }
32 |
33 | single {
34 | NotificationLocalizedStringFactory(bundle = get().bundle)
35 | }
36 |
37 | single { analyticsService }
38 |
39 | single { DefaultParseUrlViewService() }
40 | } + uiModule,
41 | )
42 |
43 | // Workaround class for injecting an `NSObject` class.
44 | // When not used, an error "KClass of Objective-C classes is not supported." is thrown.
45 | data class BundleProvider(val bundle: NSBundle)
46 |
47 | @Suppress("unused")
48 | val Koin.waitForLoadedContextModel: WaitForLoadedContextModel
49 | get() = get()
50 |
--------------------------------------------------------------------------------
/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/service/DefaultParseUrlViewService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ios.service
2 |
3 | import co.touchlab.droidcon.dto.WebLink
4 | import co.touchlab.droidcon.service.ParseUrlViewService
5 |
6 | class DefaultParseUrlViewService : ParseUrlViewService {
7 |
8 | private val urlRegex =
9 | "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)".toRegex()
10 |
11 | override fun parse(text: String): List = urlRegex.findAll(text).map { WebLink(it.range, it.value) }.toList()
12 | }
13 |
--------------------------------------------------------------------------------
/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/NotificationLocalizedStringFactory.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ios.util
2 |
3 | import co.touchlab.droidcon.application.service.NotificationSchedulingService
4 | import kotlinx.cinterop.cstr
5 | import platform.Foundation.NSBundle
6 | import platform.Foundation.NSString
7 | import platform.Foundation.stringWithFormat
8 |
9 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
10 | class NotificationLocalizedStringFactory(private val bundle: NSBundle) : NotificationSchedulingService.LocalizedStringFactory {
11 |
12 | override fun reminderTitle(roomName: String?): String {
13 | val ending = roomName?.let {
14 | NSString
15 | .stringWithFormat(
16 | bundle.localizedStringForKey("Notification.Reminder.Title.InRoom", null, null)
17 | .convertParametersForPrintf(),
18 | it.cstr,
19 | )
20 | } ?: ""
21 | return NSString
22 | .stringWithFormat(
23 | bundle.localizedStringForKey("Notification.Reminder.Title.Base", null, null)
24 | .convertParametersForPrintf(),
25 | ending.cstr,
26 | )
27 | }
28 |
29 | override fun reminderBody(sessionTitle: String): String = NSString
30 | .stringWithFormat(
31 | bundle.localizedStringForKey("Notification.Reminder.Body", null, null)
32 | .convertParametersForPrintf(),
33 | sessionTitle.cstr,
34 | )
35 |
36 | override fun feedbackTitle(): String = bundle.localizedStringForKey("Notification.Feedback.Title", null, null)
37 |
38 | override fun feedbackBody(): String = bundle.localizedStringForKey("Notification.Feedback.Body", null, null)
39 |
40 | private fun String.convertParametersForPrintf(): String = replace("%@", "%s")
41 | }
42 |
--------------------------------------------------------------------------------
/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/formatter/IOSDateFormatter.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ios.util.formatter
2 |
3 | import co.touchlab.droidcon.util.formatter.DateFormatter
4 | import kotlinx.datetime.LocalDate
5 | import kotlinx.datetime.LocalDateTime
6 | import kotlinx.datetime.toNSDateComponents
7 | import platform.Foundation.NSCalendar
8 | import platform.Foundation.NSDateFormatter
9 | import platform.Foundation.NSDateFormatterNoStyle
10 | import platform.Foundation.NSDateFormatterShortStyle
11 | import platform.Foundation.NSLocale
12 | import platform.Foundation.currentLocale
13 |
14 | class IOSDateFormatter : DateFormatter {
15 |
16 | private val monthWithDay: NSDateFormatter by lazy {
17 | NSDateFormatter().also {
18 | val dateTemplate = "MMM d"
19 | it.dateFormat = NSDateFormatter.dateFormatFromTemplate(dateTemplate, 0.toULong(), NSLocale.currentLocale)!!
20 | }
21 | }
22 |
23 | private val timeOnly: NSDateFormatter by lazy {
24 | NSDateFormatter().also {
25 | it.dateStyle = NSDateFormatterNoStyle
26 | it.timeStyle = NSDateFormatterShortStyle
27 | }
28 | }
29 |
30 | private val timeOnlyNoPeriod: NSDateFormatter by lazy {
31 | NSDateFormatter().also {
32 | val dateTemplate = "hh:mm"
33 | it.dateFormat = NSDateFormatter.dateFormatFromTemplate(dateTemplate, 0.toULong(), NSLocale.currentLocale)!!
34 | }
35 | }
36 |
37 | override fun monthWithDay(date: LocalDate) = date.date()?.let { monthWithDay.stringFromDate(it) }
38 |
39 | override fun timeOnly(dateTime: LocalDateTime) = dateTime.date()?.let { timeOnly.stringFromDate(it) }
40 |
41 | override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime) = interval(
42 | fromDateTime.date()?.let { timeOnlyNoPeriod.stringFromDate(it) },
43 | toDateTime.date()?.let { timeOnly.stringFromDate(it) },
44 | )
45 |
46 | private fun LocalDate.date() = NSCalendar.currentCalendar.dateFromComponents(toNSDateComponents())
47 |
48 | // This uses the device time zone, which is appropriate for local date/time formatting
49 | private fun LocalDateTime.date() = NSCalendar.currentCalendar.dateFromComponents(toNSDateComponents())
50 |
51 | private fun interval(from: String?, to: String?) = listOfNotNull(from, to).joinToString(" – ")
52 | }
53 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | }
8 | }
9 |
10 | dependencyResolutionManagement {
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven("https://oss.sonatype.org/content/repositories/snapshots/")
15 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
16 | maven("https://androidx.dev/storage/compose-compiler/repository/")
17 | }
18 | }
19 |
20 | plugins {
21 | id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0")
22 | }
23 |
24 | include(":shared", ":shared-ui", ":android", ":ios")
25 |
26 | rootProject.name = "Droidcon"
27 |
28 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
29 |
--------------------------------------------------------------------------------
/shared-ui/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/shared-ui/consumer-rules.pro
--------------------------------------------------------------------------------
/shared-ui/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/settings/PlatformSpecificSettings.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.settings
2 |
3 | import androidx.compose.runtime.Composable
4 | import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel
5 |
6 | @Composable
7 | internal actual fun PlatformSpecificSettingsView(viewModel: SettingsViewModel) {
8 | // Add settings specific for Android here.
9 | }
10 |
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/theme/Type.android.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.theme
2 |
3 | import androidx.compose.ui.text.font.Font
4 | import androidx.compose.ui.text.font.FontFamily
5 | import androidx.compose.ui.text.font.FontWeight
6 | import co.touchlab.droidcon.sharedui.R
7 |
8 | internal val montserratLightFont = Font(R.font.montserrat_regular, FontWeight.Light)
9 | internal val montserratRegularFont = Font(R.font.montserrat_regular, FontWeight.Normal)
10 | internal val montserratMediumFont = Font(R.font.montserrat_medium, FontWeight.Medium)
11 | internal val montserratSemiBoldFont = Font(R.font.montserrat_semi_bold, FontWeight.SemiBold)
12 | internal val montserratBoldFont = Font(R.font.montserrat_semi_bold, FontWeight.Bold)
13 |
14 | actual val montserratFontFamily: FontFamily = FontFamily(
15 | montserratLightFont,
16 | montserratRegularFont,
17 | montserratMediumFont,
18 | montserratSemiBoldFont,
19 | montserratBoldFont,
20 | )
21 |
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/Dialog.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.ExperimentalComposeUiApi
5 | import androidx.compose.ui.window.Dialog as AndroidXComposeDialog
6 | import androidx.compose.ui.window.DialogProperties
7 |
8 | @OptIn(ExperimentalComposeUiApi::class)
9 | @Composable
10 | internal actual fun Dialog(dismiss: () -> Unit, content: @Composable () -> Unit) {
11 | AndroidXComposeDialog(
12 | onDismissRequest = dismiss,
13 | properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false, usePlatformDefaultWidth = false),
14 | ) {
15 | content()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("ktlint:standard:filename")
2 |
3 | package co.touchlab.droidcon.ui.util
4 |
5 | import android.annotation.SuppressLint
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.filled.Warning
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.platform.LocalContext
22 | import androidx.compose.ui.res.painterResource
23 | import co.touchlab.droidcon.ui.theme.Dimensions
24 |
25 | // Use of the function getIdentifier is discouraged, but we need to use it since the drawable names are defined in the common code for both
26 | // platforms and on each platform we need to get the drawable according to provided name.
27 | @SuppressLint("ComposableNaming", "DiscouragedApi")
28 | @Composable
29 | internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) {
30 | val context = LocalContext.current
31 | val imageRes = context.resources.getIdentifier(imageResourceName, "drawable", context.packageName).takeIf { it != 0 }
32 | if (imageRes != null) {
33 | androidx.compose.foundation.Image(
34 | modifier = modifier,
35 | painter = painterResource(id = imageRes),
36 | contentDescription = contentDescription,
37 | contentScale = ContentScale.FillWidth,
38 | )
39 | } else {
40 | Row(
41 | modifier = modifier.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(Dimensions.Padding.half)),
42 | verticalAlignment = Alignment.CenterVertically,
43 | ) {
44 | Spacer(modifier = Modifier.weight(1f))
45 | Icon(
46 | imageVector = Icons.Default.Warning,
47 | contentDescription = contentDescription,
48 | modifier = Modifier.padding(Dimensions.Padding.half),
49 | tint = Color.White,
50 | )
51 | Text("Image not supported", modifier = Modifier.padding(Dimensions.Padding.default), color = Color.White)
52 | Spacer(modifier = Modifier.weight(1f))
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.foundation.layout.systemBarsPadding
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import co.touchlab.droidcon.ui.MainComposeView
7 | import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel
8 |
9 | @Composable
10 | fun MainView(waitForLoadedContextModel: WaitForLoadedContextModel) {
11 | MainComposeView(waitForLoadedContextModel = waitForLoadedContextModel, modifier = Modifier.systemBarsPadding())
12 | }
13 |
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/NavigationBackPressWrapper.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | internal actual fun NavigationBackPressWrapper(content: @Composable () -> Unit) {
7 | // For now no back press wrapping is needed on Android.
8 | content()
9 | }
10 |
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/res/font/montserrat_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/shared-ui/src/androidMain/res/font/montserrat_bold.ttf
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/res/font/montserrat_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/shared-ui/src/androidMain/res/font/montserrat_light.ttf
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/res/font/montserrat_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/shared-ui/src/androidMain/res/font/montserrat_medium.ttf
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/res/font/montserrat_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/shared-ui/src/androidMain/res/font/montserrat_regular.ttf
--------------------------------------------------------------------------------
/shared-ui/src/androidMain/res/font/montserrat_semi_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/shared-ui/src/androidMain/res/font/montserrat_semi_bold.ttf
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/composeResources/drawable/venue-map-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/shared-ui/src/commonMain/composeResources/drawable/venue-map-1.jpg
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/PlatformSpecificSettings.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.settings
2 |
3 | import androidx.compose.runtime.Composable
4 | import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel
5 |
6 | @Composable
7 | internal expect fun PlatformSpecificSettingsView(viewModel: SettingsViewModel)
8 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/theme/Dimensions.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.theme
2 |
3 | import androidx.compose.ui.unit.dp
4 |
5 | object Dimensions {
6 | object Padding {
7 |
8 | val quarter = 4.dp
9 | val half = 8.dp
10 | val default = 16.dp
11 | val double = 32.dp
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.theme
2 |
3 | import androidx.compose.ui.text.font.FontFamily
4 |
5 | expect val montserratFontFamily: FontFamily
6 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/theme/Typography.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontWeight
6 | import androidx.compose.ui.unit.sp
7 |
8 | internal object Typography {
9 | val typography = Typography(
10 | headlineLarge = TextStyle(
11 | fontFamily = montserratFontFamily,
12 | fontWeight = FontWeight.Medium,
13 | fontSize = 32.sp,
14 | ),
15 | headlineMedium = TextStyle(
16 | fontFamily = montserratFontFamily,
17 | fontWeight = FontWeight.Medium,
18 | fontSize = 28.sp,
19 | ),
20 | headlineSmall = TextStyle(
21 | fontFamily = montserratFontFamily,
22 | fontWeight = FontWeight.Medium,
23 | fontSize = 22.sp,
24 | letterSpacing = 0.15.sp,
25 | ),
26 | titleLarge = TextStyle(
27 | fontFamily = montserratFontFamily,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 20.sp,
30 | ),
31 | titleMedium = TextStyle(
32 | fontFamily = montserratFontFamily,
33 | fontWeight = FontWeight.Medium,
34 | fontSize = 16.sp,
35 | ),
36 | titleSmall = TextStyle(
37 | fontFamily = montserratFontFamily,
38 | fontWeight = FontWeight.Medium,
39 | fontSize = 14.sp,
40 | ),
41 | bodyLarge = TextStyle(
42 | fontFamily = montserratFontFamily,
43 | fontWeight = FontWeight.Medium,
44 | fontSize = 16.sp,
45 | ),
46 | bodyMedium = TextStyle(
47 | fontFamily = montserratFontFamily,
48 | fontWeight = FontWeight.Medium,
49 | fontSize = 14.sp,
50 | ),
51 | bodySmall = TextStyle(
52 | fontFamily = montserratFontFamily,
53 | fontWeight = FontWeight.Medium,
54 | fontSize = 12.sp,
55 | ),
56 | labelLarge = TextStyle(
57 | fontFamily = montserratFontFamily,
58 | fontWeight = FontWeight.SemiBold,
59 | fontSize = 12.sp,
60 | ),
61 | labelMedium = TextStyle(
62 | fontFamily = montserratFontFamily,
63 | fontWeight = FontWeight.Medium,
64 | fontSize = 12.sp,
65 | ),
66 | labelSmall = TextStyle(
67 | fontFamily = montserratFontFamily,
68 | fontWeight = FontWeight.Medium,
69 | fontSize = 10.sp,
70 | letterSpacing = 1.5.sp,
71 | ),
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | internal expect fun Dialog(dismiss: () -> Unit, content: @Composable () -> Unit)
7 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import co.touchlab.kermit.Logger
6 | import coil3.ImageLoader
7 | import coil3.PlatformContext
8 | import coil3.compose.AsyncImage
9 | import coil3.request.crossfade
10 | import coil3.util.DebugLogger
11 |
12 | @Composable
13 | fun DcAsyncImage(logTag: String, model: Any?, contentDescription: String?, modifier: Modifier = Modifier) {
14 | AsyncImage(
15 | modifier = modifier,
16 | model = model,
17 | contentDescription = contentDescription,
18 | onError = {
19 | Logger.e(
20 | messageString = logTag,
21 | throwable = it.result.throwable,
22 | tag = "AsyncImage OnError Request = ${it.result.request}\n",
23 | )
24 | },
25 | )
26 | }
27 |
28 | fun dcImageLoader(context: PlatformContext, debug: Boolean = false): ImageLoader = ImageLoader.Builder(context)
29 | .crossfade(true)
30 | .apply {
31 | if (debug) {
32 | logger(DebugLogger())
33 | }
34 | }
35 | .build()
36 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 |
6 | @Composable
7 | internal expect fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?)
8 |
9 | @Composable
10 | internal fun LocalImage(imageResourceName: String, modifier: Modifier = Modifier, contentDescription: String? = null) {
11 | __LocalImage(imageResourceName, modifier, contentDescription)
12 | }
13 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | internal expect fun NavigationBackPressWrapper(content: @Composable () -> Unit)
7 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/ObserveAsState.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.neverEqualPolicy
8 | import androidx.compose.runtime.remember
9 | import org.brightify.hyperdrive.multiplatformx.ManageableViewModel
10 | import org.brightify.hyperdrive.multiplatformx.ObservableObject
11 | import org.brightify.hyperdrive.multiplatformx.property.ObservableProperty
12 |
13 | /**
14 | * Observe a view model as its properties change to update the view.
15 | *
16 | * Equivalent to [ObservableProperty.observeAsState] for observing all changes in a view model.
17 | */
18 | @Composable
19 | internal fun T.observeAsState(): State {
20 | val result = remember(this) { mutableStateOf(this, neverEqualPolicy()) }
21 | val listener = remember(this) {
22 | object : ObservableObject.ChangeTracking.Listener {
23 | override fun onObjectDidChange() {
24 | result.value = this@observeAsState
25 | }
26 | }
27 | }
28 | DisposableEffect(this) {
29 | val token = changeTracking.addListener(listener)
30 | result.value = this@observeAsState
31 |
32 | onDispose {
33 | token.cancel()
34 | }
35 | }
36 | return result
37 | }
38 |
39 | /**
40 | * Observe a view model property as it changes to update the view.
41 | *
42 | * Equivalent to [collectAsState] for [ObservableProperty].
43 | */
44 | @Composable
45 | internal fun ObservableProperty.observeAsState(): State {
46 | val result = remember(this) { mutableStateOf(value, neverEqualPolicy()) }
47 | val listener = remember(this) {
48 | object : ObservableProperty.Listener {
49 | override fun valueDidChange(oldValue: T, newValue: T) {
50 | result.value = newValue
51 | }
52 | }
53 | }
54 | DisposableEffect(this) {
55 | val token = addListener(listener)
56 | result.value = value
57 |
58 | onDispose {
59 | token.cancel()
60 | }
61 | }
62 | return result
63 | }
64 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.venue
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.Scaffold
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import com.github.panpf.zoomimage.ZoomImage
9 | import droidcon.shared_ui.generated.resources.venue_map_1
10 | import org.jetbrains.compose.resources.painterResource
11 |
12 | @Composable
13 | fun VenueView() {
14 | Scaffold { paddingValues ->
15 | VenueBodyView(
16 | modifier = Modifier.padding(paddingValues),
17 | )
18 | }
19 | }
20 |
21 | @Composable
22 | fun VenueBodyView(modifier: Modifier = Modifier) {
23 | ZoomImage(
24 | painter = painterResource(droidcon.shared_ui.generated.resources.Res.drawable.venue_map_1),
25 | contentDescription = null,
26 | modifier = modifier.fillMaxSize(),
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/LocalDateTime+startOfMinute.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("ktlint:standard:filename")
2 |
3 | package co.touchlab.droidcon.util
4 |
5 | import kotlinx.datetime.LocalDateTime
6 |
7 | val LocalDateTime.startOfMinute: LocalDateTime
8 | get() = LocalDateTime(year, month, dayOfMonth, hour, minute)
9 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel
2 |
3 | import co.touchlab.droidcon.application.gateway.SettingsGateway
4 | import co.touchlab.droidcon.domain.entity.Conference
5 | import co.touchlab.droidcon.domain.service.ConferenceConfigProvider
6 | import co.touchlab.droidcon.domain.service.SyncService
7 | import co.touchlab.kermit.Logger
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.IO
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.withContext
14 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
15 |
16 | class WaitForLoadedContextModel(
17 | private val conferenceConfigProvider: ConferenceConfigProvider,
18 | applicationViewModelFactory: ApplicationViewModel.Factory,
19 | private val syncService: SyncService,
20 | private val settingsGateway: SettingsGateway,
21 | ) : BaseViewModel() {
22 | sealed interface State {
23 | data object Loading : State
24 | data class Ready(val conference: Conference) : State
25 | }
26 |
27 | private val _state = MutableStateFlow(State.Loading)
28 | val state: StateFlow = _state
29 | val applicationViewModel by managed(applicationViewModelFactory.create())
30 |
31 | private val log = Logger.withTag("WaitForLoadedContextModel")
32 |
33 | suspend fun monitorConferenceChanges() {
34 | conferenceConfigProvider.loadSelectedConference()
35 | }
36 |
37 | suspend fun watchConferenceChanges() {
38 | lifecycle.whileAttached {
39 | withContext(Dispatchers.IO) {
40 | try {
41 | syncService.syncConferences()
42 | } catch (e: Exception) {
43 | log.e(e) { "Failed to sync conferences" }
44 | }
45 | }
46 |
47 | launch {
48 | conferenceConfigProvider.observeChanges().collect { conference ->
49 | _state.emit(State.Ready(conference))
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.session
2 |
3 | import co.touchlab.droidcon.domain.gateway.SessionGateway
4 | import co.touchlab.droidcon.domain.service.ConferenceConfigProvider
5 | import co.touchlab.droidcon.domain.service.DateTimeService
6 |
7 | class AgendaViewModel(
8 | sessionGateway: SessionGateway,
9 | sessionDayFactory: SessionDayViewModel.Factory,
10 | sessionDetailFactory: SessionDetailViewModel.Factory,
11 | sessionDetailScrollStateStorage: SessionDetailScrollStateStorage,
12 | dateTimeService: DateTimeService,
13 | conferenceConfigProvider: ConferenceConfigProvider,
14 | ) : BaseSessionListViewModel(
15 | sessionGateway,
16 | sessionDayFactory,
17 | sessionDetailFactory,
18 | sessionDetailScrollStateStorage,
19 | dateTimeService,
20 | conferenceConfigProvider,
21 | attendingOnly = true,
22 | ) {
23 | class Factory(
24 | private val sessionGateway: SessionGateway,
25 | private val sessionDayFactory: SessionDayViewModel.Factory,
26 | private val sessionDetailFactory: SessionDetailViewModel.Factory,
27 | private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage,
28 | private val dateTimeService: DateTimeService,
29 | private val conferenceConfigProvider: ConferenceConfigProvider,
30 | ) {
31 |
32 | fun create() = AgendaViewModel(
33 | sessionGateway,
34 | sessionDayFactory,
35 | sessionDetailFactory,
36 | sessionDetailScrollStateStorage,
37 | dateTimeService,
38 | conferenceConfigProvider,
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.session
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 | import co.touchlab.droidcon.domain.gateway.SessionGateway
5 | import co.touchlab.droidcon.domain.service.ConferenceConfigProvider
6 | import co.touchlab.droidcon.domain.service.DateTimeService
7 |
8 | class ScheduleViewModel(
9 | private val sessionGateway: SessionGateway,
10 | sessionDayFactory: SessionDayViewModel.Factory,
11 | private val sessionDetailFactory: SessionDetailViewModel.Factory,
12 | sessionDetailScrollStateStorage: SessionDetailScrollStateStorage,
13 | dateTimeService: DateTimeService,
14 | conferenceConfigProvider: ConferenceConfigProvider,
15 | ) : BaseSessionListViewModel(
16 | sessionGateway,
17 | sessionDayFactory,
18 | sessionDetailFactory,
19 | sessionDetailScrollStateStorage,
20 | dateTimeService,
21 | conferenceConfigProvider,
22 | attendingOnly = false,
23 | ) {
24 |
25 | fun openSessionDetail(sessionId: Session.Id) {
26 | lifecycle.whileAttached {
27 | val sessionItem = sessionGateway.getScheduleItem(sessionId) ?: return@whileAttached
28 | presentedSessionDetail = sessionDetailFactory.create(sessionItem)
29 | }
30 | }
31 |
32 | class Factory(
33 | private val sessionGateway: SessionGateway,
34 | private val sessionDayFactory: SessionDayViewModel.Factory,
35 | private val sessionDetailFactory: SessionDetailViewModel.Factory,
36 | private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage,
37 | private val dateTimeService: DateTimeService,
38 | private val conferenceConfigProvider: ConferenceConfigProvider,
39 | ) {
40 |
41 | fun create() = ScheduleViewModel(
42 | sessionGateway,
43 | sessionDayFactory,
44 | sessionDetailFactory,
45 | sessionDetailScrollStateStorage,
46 | dateTimeService,
47 | conferenceConfigProvider,
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.session
2 |
3 | import co.touchlab.droidcon.domain.composite.ScheduleItem
4 | import co.touchlab.droidcon.util.formatter.DateFormatter
5 | import kotlinx.datetime.LocalDateTime
6 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
7 |
8 | class SessionBlockViewModel(
9 | sessionListItemFactory: SessionListItemViewModel.Factory,
10 | dateFormatter: DateFormatter,
11 | startsAt: LocalDateTime,
12 | items: List,
13 | onScheduleItemSelected: (ScheduleItem) -> Unit,
14 | ) : BaseViewModel() {
15 | val time: String = dateFormatter.timeOnly(startsAt) ?: ""
16 | val sessions: List by managedList(
17 | items.map { item ->
18 | sessionListItemFactory.create(
19 | item,
20 | selected = {
21 | onScheduleItemSelected(item)
22 | },
23 | )
24 | },
25 | )
26 | val observeSessions by observe(::sessions)
27 |
28 | class Factory(private val sessionListItemFactory: SessionListItemViewModel.Factory, private val dateFormatter: DateFormatter) {
29 | fun create(startsAt: LocalDateTime, items: List, onScheduleItemSelected: (ScheduleItem) -> Unit) =
30 | SessionBlockViewModel(sessionListItemFactory, dateFormatter, startsAt, items, onScheduleItemSelected)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailScrollStateStorage.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.session
2 |
3 | import kotlinx.datetime.LocalDate
4 |
5 | class SessionDetailScrollStateStorage {
6 |
7 | val scrollStates: MutableMap = mutableMapOf()
8 | val agendaScrollStates: MutableMap = mutableMapOf()
9 | var selectedDay: LocalDate? = null
10 |
11 | fun getScrollState(day: LocalDate, agenda: Boolean): SessionDayViewModel.ScrollState = if (agenda) {
12 | agendaScrollStates[day]
13 | } else {
14 | scrollStates[day]
15 | } ?: SessionDayViewModel.ScrollState(firstVisibleItemIndex = 0, firstVisibleItemScrollOffset = 0)
16 |
17 | fun setScrollState(day: LocalDate, agenda: Boolean, scrollState: SessionDayViewModel.ScrollState) {
18 | if (agenda) {
19 | agendaScrollStates[day] = scrollState
20 | } else {
21 | scrollStates[day] = scrollState
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionListItemViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.session
2 |
3 | import co.touchlab.droidcon.domain.composite.ScheduleItem
4 | import co.touchlab.droidcon.domain.service.DateTimeService
5 | import kotlinx.coroutines.delay
6 | import kotlinx.coroutines.flow.flow
7 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
8 |
9 | class SessionListItemViewModel(dateTimeService: DateTimeService, item: ScheduleItem, val selected: () -> Unit) : BaseViewModel() {
10 | val title: String = item.session.title
11 | val isServiceSession: Boolean = item.session.isServiceSession
12 | val isAttending: Boolean = item.session.rsvp.isAttending
13 | val isInConflict: Boolean = item.isInConflict
14 | val speakers: String = item.speakers.joinToString { it.fullName }
15 | val room: String? = item.room?.name
16 |
17 | val isInPast: Boolean by collected(
18 | dateTimeService.now() > item.session.endsAt,
19 | flow {
20 | while (true) {
21 | val isInPast = dateTimeService.now() > item.session.endsAt
22 | emit(isInPast)
23 | delay(10_000)
24 | }
25 | },
26 | )
27 | val observeIsInPast by observe(::isInPast)
28 |
29 | class Factory(private val dateTimeService: DateTimeService) {
30 | fun create(item: ScheduleItem, selected: () -> Unit) = SessionListItemViewModel(dateTimeService, item, selected)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.session
2 |
3 | import co.touchlab.droidcon.composite.Url
4 | import co.touchlab.droidcon.domain.entity.Profile
5 | import co.touchlab.droidcon.dto.WebLink
6 | import co.touchlab.droidcon.service.ParseUrlViewService
7 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
8 |
9 | class SpeakerDetailViewModel(private val parseUrlViewService: ParseUrlViewService, profile: Profile) : BaseViewModel() {
10 |
11 | val avatarUrl = profile.profilePicture
12 |
13 | val name = profile.fullName
14 | val position = profile.tagLine
15 |
16 | val socials = Socials(
17 | website = profile.website,
18 | twitter = profile.twitter,
19 | linkedIn = profile.linkedIn,
20 | )
21 |
22 | val bio = profile.bio
23 | val bioWebLinks: List = bio?.let(parseUrlViewService::parse) ?: emptyList()
24 |
25 | data class Socials(val website: Url?, val twitter: Url?, val linkedIn: Url?) {
26 |
27 | val isEmpty: Boolean = listOfNotNull(
28 | website,
29 | twitter,
30 | linkedIn,
31 | ).isEmpty()
32 | }
33 |
34 | class Factory(private val parseUrlViewService: ParseUrlViewService) {
35 |
36 | fun create(profile: Profile) = SpeakerDetailViewModel(parseUrlViewService, profile)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerListItemViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.session
2 |
3 | import co.touchlab.droidcon.domain.entity.Profile
4 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
5 |
6 | class SpeakerListItemViewModel(profile: Profile, val selected: () -> Unit) : BaseViewModel() {
7 | val avatarUrl = profile.profilePicture
8 | val info = listOfNotNull(profile.fullName, profile.tagLine).joinToString()
9 | val bio = profile.bio
10 |
11 | class Factory {
12 | fun create(profile: Profile, selected: () -> Unit) = SpeakerListItemViewModel(profile, selected)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/AboutItemViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.settings
2 |
3 | import co.touchlab.droidcon.dto.WebLink
4 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
5 |
6 | class AboutItemViewModel(val title: String, val detail: String, val webLinks: List, val icon: String) : BaseViewModel()
7 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/AboutViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.settings
2 |
3 | import co.touchlab.droidcon.application.composite.AboutItem
4 | import co.touchlab.droidcon.application.repository.AboutRepository
5 | import co.touchlab.droidcon.service.ParseUrlViewService
6 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
7 |
8 | class AboutViewModel(private val aboutRepository: AboutRepository, private val parseUrlViewService: ParseUrlViewService) : BaseViewModel() {
9 |
10 | var items: List by published(emptyList())
11 | private set
12 |
13 | var itemViewModels: List by published(emptyList())
14 | val observeItemViewModels by observe(::itemViewModels)
15 |
16 | override suspend fun whileAttached() {
17 | items = aboutRepository.getAboutItems()
18 | itemViewModels = items.map {
19 | val links = parseUrlViewService.parse(it.detail)
20 | AboutItemViewModel(it.title, it.detail, links, it.icon)
21 | }
22 | }
23 |
24 | class Factory(private val aboutRepository: AboutRepository, private val parseUrlViewService: ParseUrlViewService) {
25 |
26 | fun create() = AboutViewModel(aboutRepository, parseUrlViewService)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.sponsor
2 |
3 | import co.touchlab.droidcon.domain.entity.Sponsor
4 | import co.touchlab.droidcon.domain.gateway.SponsorGateway
5 | import co.touchlab.droidcon.viewmodel.session.SpeakerDetailViewModel
6 | import co.touchlab.droidcon.viewmodel.session.SpeakerListItemViewModel
7 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
8 |
9 | class SponsorDetailViewModel(
10 | private val sponsorGateway: SponsorGateway,
11 | private val speakerListItemFactory: SpeakerListItemViewModel.Factory,
12 | private val speakerDetailFactory: SpeakerDetailViewModel.Factory,
13 | private val sponsor: Sponsor,
14 | val groupName: String,
15 | ) : BaseViewModel() {
16 |
17 | val name = sponsor.name
18 | val imageUrl = sponsor.icon
19 |
20 | val abstract = sponsor.description
21 |
22 | val representatives: List by managedList(emptyList())
23 | val observeRepresentatives by observe(::representatives)
24 |
25 | var presentedSpeakerDetail: SpeakerDetailViewModel? by managed(null)
26 | val observePresentedSpeakerDetail by observe(::presentedSpeakerDetail)
27 |
28 | override suspend fun whileAttached() {
29 | sponsorGateway.getRepresentatives(sponsor.id).map { speaker ->
30 | speakerListItemFactory.create(
31 | speaker,
32 | selected = {
33 | presentedSpeakerDetail = speakerDetailFactory.create(speaker)
34 | },
35 | )
36 | }
37 | }
38 |
39 | class Factory(
40 | private val sponsorGateway: SponsorGateway,
41 | private val speakerListItemFactory: SpeakerListItemViewModel.Factory,
42 | private val speakerDetailFactory: SpeakerDetailViewModel.Factory,
43 | ) {
44 |
45 | fun create(sponsor: Sponsor, groupName: String) =
46 | SponsorDetailViewModel(sponsorGateway, speakerListItemFactory, speakerDetailFactory, sponsor, groupName)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupItemViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.sponsor
2 |
3 | import co.touchlab.droidcon.domain.entity.Sponsor
4 | import io.ktor.http.URLParserException
5 | import io.ktor.http.Url
6 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
7 |
8 | class SponsorGroupItemViewModel(private val sponsor: Sponsor, val selected: () -> Unit) : BaseViewModel() {
9 |
10 | val name = sponsor.name
11 | val imageUrl = sponsor.icon
12 |
13 | val validImageUrl: String? =
14 | try {
15 | Url(sponsor.icon.string).toString()
16 | } catch (e: URLParserException) {
17 | null
18 | }
19 |
20 | class Factory {
21 |
22 | fun create(sponsor: Sponsor, selected: () -> Unit) = SponsorGroupItemViewModel(sponsor, selected)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.sponsor
2 |
3 | import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors
4 | import co.touchlab.droidcon.domain.entity.Sponsor
5 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
6 |
7 | class SponsorGroupViewModel(
8 | sponsorGroupItemFactory: SponsorGroupItemViewModel.Factory,
9 | sponsorGroup: SponsorGroupWithSponsors,
10 | onSponsorSelected: (Sponsor) -> Unit,
11 | ) : BaseViewModel() {
12 | val title = sponsorGroup.group.name
13 | val isProminent = sponsorGroup.group.isProminent
14 | val sponsors by managedList(
15 | sponsorGroup.sponsors.map { sponsor ->
16 | sponsorGroupItemFactory.create(sponsor, selected = { onSponsorSelected(sponsor) })
17 | },
18 | )
19 | val observeSponsors by observe(::sponsors)
20 |
21 | class Factory(private val sponsorGroupItemFactory: SponsorGroupItemViewModel.Factory) {
22 | fun create(sponsorGroup: SponsorGroupWithSponsors, onSponsorSelected: (Sponsor) -> Unit) =
23 | SponsorGroupViewModel(sponsorGroupItemFactory, sponsorGroup, onSponsorSelected)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.viewmodel.sponsor
2 |
3 | import co.touchlab.droidcon.composite.Url
4 | import co.touchlab.droidcon.domain.gateway.SponsorGateway
5 | import kotlinx.coroutines.flow.map
6 | import org.brightify.hyperdrive.multiplatformx.BaseViewModel
7 |
8 | class SponsorListViewModel(
9 | private val sponsorGateway: SponsorGateway,
10 | private val sponsorGroupFactory: SponsorGroupViewModel.Factory,
11 | private val sponsorDetailFactory: SponsorDetailViewModel.Factory,
12 | ) : BaseViewModel() {
13 | val sponsorGroups: List by managedList(
14 | emptyList(),
15 | sponsorGateway.observeSponsors()
16 | .map { sponsorGroups ->
17 | sponsorGroups
18 | .sortedBy { it.group.displayPriority }
19 | .map { sponsorGroup ->
20 | sponsorGroupFactory.create(
21 | sponsorGroup,
22 | onSponsorSelected = { sponsor ->
23 | if (sponsor.hasDetail) {
24 | presentedSponsorDetail = sponsorDetailFactory.create(sponsor, sponsorGroup.group.name)
25 | } else {
26 | presentedUrl = sponsor.url
27 | }
28 | },
29 | )
30 | }
31 | },
32 | )
33 | val observeSponsorGroups by observe(::sponsorGroups)
34 |
35 | var presentedSponsorDetail: SponsorDetailViewModel? by managed(null)
36 | val observePresentedSponsorDetail by observe(::presentedSponsorDetail)
37 |
38 | var presentedUrl: Url? by published(null)
39 | val observePresentedUrl by observe(::presentedUrl)
40 |
41 | class Factory(
42 | private val sponsorGateway: SponsorGateway,
43 | private val sponsorGroupFactory: SponsorGroupViewModel.Factory,
44 | private val sponsorDetailFactory: SponsorDetailViewModel.Factory,
45 | ) {
46 |
47 | fun create() = SponsorListViewModel(sponsorGateway, sponsorGroupFactory, sponsorDetailFactory)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui
2 |
3 | import androidx.compose.ui.window.ComposeUIViewController
4 | import co.touchlab.droidcon.ui.venue.VenueBodyView
5 | import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel
6 |
7 | @Suppress("unused")
8 | fun getRootController(viewModel: WaitForLoadedContextModel) = ComposeUIViewController {
9 | MainComposeView(viewModel)
10 | }
11 |
12 | @Suppress("unused")
13 | fun venueBodyViewController() = ComposeUIViewController {
14 | VenueBodyView()
15 | }
16 |
--------------------------------------------------------------------------------
/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/settings/PlatformSpecificSettings.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.settings
2 |
3 | import androidx.compose.runtime.Composable
4 | import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel
5 |
6 | @Composable
7 | internal actual fun PlatformSpecificSettingsView(viewModel: SettingsViewModel) {
8 | // No platform-specific settings needed for iOS
9 | }
10 |
--------------------------------------------------------------------------------
/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/theme/Type.ios.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.theme
2 |
3 | import androidx.compose.ui.text.font.FontFamily
4 |
5 | actual val montserratFontFamily: FontFamily = FontFamily.Default
6 |
--------------------------------------------------------------------------------
/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/Dialog.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.interaction.MutableInteractionSource
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Color
12 |
13 | @Composable
14 | internal actual fun Dialog(dismiss: () -> Unit, content: @Composable () -> Unit) {
15 | Box(
16 | modifier = Modifier
17 | .fillMaxSize()
18 | .background(Color.Black.copy(alpha = 0.3f))
19 | .clickable(interactionSource = MutableInteractionSource(), indication = null) { },
20 | contentAlignment = Alignment.Center,
21 | ) {
22 | content()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Warning
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.graphics.painter.BitmapPainter
19 | import androidx.compose.ui.graphics.toComposeImageBitmap
20 | import androidx.compose.ui.layout.ContentScale
21 | import co.touchlab.droidcon.ui.theme.Dimensions
22 | import platform.UIKit.UIImage
23 |
24 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
25 | @Composable
26 | internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) {
27 | val painter = remember { UIImage.imageNamed(imageResourceName)?.toSkiaImage()?.toComposeImageBitmap()?.let(::BitmapPainter) }
28 | if (painter != null) {
29 | androidx.compose.foundation.Image(
30 | modifier = modifier,
31 | painter = painter,
32 | contentDescription = contentDescription,
33 | contentScale = ContentScale.FillWidth,
34 | )
35 | } else {
36 | Row(
37 | modifier = modifier.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(Dimensions.Padding.half)),
38 | verticalAlignment = Alignment.CenterVertically,
39 | ) {
40 | Spacer(modifier = Modifier.weight(1f))
41 | Icon(
42 | imageVector = Icons.Default.Warning,
43 | contentDescription = contentDescription,
44 | modifier = Modifier.padding(Dimensions.Padding.half),
45 | tint = Color.White,
46 | )
47 | Text("Image not supported", modifier = Modifier.padding(Dimensions.Padding.default), color = Color.White)
48 | Spacer(modifier = Modifier.weight(1f))
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/ToSkiaImage.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.ui.util
2 |
3 | import kotlinx.cinterop.ExperimentalForeignApi
4 | import kotlinx.cinterop.get
5 | import org.jetbrains.skia.ColorAlphaType
6 | import org.jetbrains.skia.ColorType
7 | import org.jetbrains.skia.Image
8 | import org.jetbrains.skia.ImageInfo
9 | import platform.CoreFoundation.CFDataGetBytePtr
10 | import platform.CoreFoundation.CFDataGetLength
11 | import platform.CoreFoundation.CFRelease
12 | import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB
13 | import platform.CoreGraphics.CGDataProviderCopyData
14 | import platform.CoreGraphics.CGImageAlphaInfo
15 | import platform.CoreGraphics.CGImageCreateCopyWithColorSpace
16 | import platform.CoreGraphics.CGImageGetAlphaInfo
17 | import platform.CoreGraphics.CGImageGetBytesPerRow
18 | import platform.CoreGraphics.CGImageGetDataProvider
19 | import platform.CoreGraphics.CGImageGetHeight
20 | import platform.CoreGraphics.CGImageGetWidth
21 | import platform.UIKit.UIImage
22 |
23 | // TODO: Add support for remaining color spaces when the Skia library supports them.
24 | @ExperimentalForeignApi
25 | internal fun UIImage.toSkiaImage(): Image? {
26 | val imageRef = CGImageCreateCopyWithColorSpace(this.CGImage, CGColorSpaceCreateDeviceRGB()) ?: return null
27 |
28 | val width = CGImageGetWidth(imageRef).toInt()
29 | val height = CGImageGetHeight(imageRef).toInt()
30 |
31 | val bytesPerRow = CGImageGetBytesPerRow(imageRef)
32 | val data = CGDataProviderCopyData(CGImageGetDataProvider(imageRef))
33 | val bytePointer = CFDataGetBytePtr(data)
34 | val length = CFDataGetLength(data)
35 | val alphaInfo = CGImageGetAlphaInfo(imageRef)
36 |
37 | val alphaType = when (alphaInfo) {
38 | CGImageAlphaInfo.kCGImageAlphaPremultipliedFirst, CGImageAlphaInfo.kCGImageAlphaPremultipliedLast -> ColorAlphaType.PREMUL
39 | CGImageAlphaInfo.kCGImageAlphaFirst, CGImageAlphaInfo.kCGImageAlphaLast -> ColorAlphaType.UNPREMUL
40 | CGImageAlphaInfo.kCGImageAlphaNone, CGImageAlphaInfo.kCGImageAlphaNoneSkipFirst, CGImageAlphaInfo.kCGImageAlphaNoneSkipLast,
41 | -> ColorAlphaType.OPAQUE
42 |
43 | else -> ColorAlphaType.UNKNOWN
44 | }
45 |
46 | val byteArray = ByteArray(length.toInt()) { index ->
47 | bytePointer!![index].toByte()
48 | }
49 | CFRelease(data)
50 | CFRelease(imageRef)
51 |
52 | return Image.makeRaster(
53 | imageInfo = ImageInfo(width = width, height = height, colorType = ColorType.RGBA_8888, alphaType = alphaType),
54 | bytes = byteArray,
55 | rowBytes = bytesPerRow.toInt(),
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/shared/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/shared/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/shared/consumer-rules.pro
--------------------------------------------------------------------------------
/shared/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/shared/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/Koin.android.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 | import co.touchlab.droidcon.application.service.NotificationService
5 | import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory
6 | import co.touchlab.droidcon.service.AndroidNotificationService
7 | import co.touchlab.droidcon.util.formatter.AndroidDateFormatter
8 | import co.touchlab.droidcon.util.formatter.DateFormatter
9 | import co.touchlab.kermit.ExperimentalKermitApi
10 | import co.touchlab.kermit.LogcatWriter
11 | import co.touchlab.kermit.Logger
12 | import co.touchlab.kermit.StaticConfig
13 | import co.touchlab.kermit.crashlytics.CrashlyticsLogWriter
14 | import com.russhwolf.settings.ExperimentalSettingsApi
15 | import io.ktor.client.engine.HttpClientEngine
16 | import io.ktor.client.engine.okhttp.OkHttp
17 | import org.koin.core.module.Module
18 | import org.koin.dsl.module
19 |
20 | @OptIn(ExperimentalSettingsApi::class, ExperimentalKermitApi::class)
21 | actual val platformModule: Module = module {
22 | single {
23 | SqlDelightDriverFactory(context = get()).createDriver()
24 | }
25 |
26 | single {
27 | OkHttp.create {}
28 | }
29 |
30 | single {
31 | AndroidNotificationService(
32 | context = get(),
33 | entrypointActivity = get(),
34 | log = getWith("AndroidNotificationService"),
35 | syncService = get(),
36 | conferenceRepository = get(),
37 | settings = get(),
38 | json = get(),
39 | )
40 | }
41 | single {
42 | get()
43 | }
44 |
45 | single {
46 | AndroidDateFormatter(
47 | dateTimeService = get(),
48 | conferenceConfigProvider = get(),
49 | )
50 | }
51 |
52 | val baseKermit = Logger(config = StaticConfig(logWriterList = listOf(LogcatWriter(), CrashlyticsLogWriter())), tag = "Droidcon")
53 | factory { (tag: String?) -> if (tag != null) baseKermit.withTag(tag) else baseKermit }
54 | }
55 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository.impl
2 |
3 | import android.content.Context
4 | import app.cash.sqldelight.db.SqlDriver
5 | import app.cash.sqldelight.driver.android.AndroidSqliteDriver
6 | import co.touchlab.droidcon.db.DroidconDatabase
7 |
8 | actual class SqlDelightDriverFactory(private val context: Context) {
9 | actual fun createDriver(): SqlDriver = AndroidSqliteDriver(DroidconDatabase.Schema, context, "droidcon.db")
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/service/NotificationPublisher.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.service
2 |
3 | import android.app.Notification
4 | import android.app.NotificationManager
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Intent
8 |
9 | class NotificationPublisher : BroadcastReceiver() {
10 |
11 | override fun onReceive(context: Context, intent: Intent) {
12 | val notificationId = intent.getIntExtra(AndroidNotificationService.NOTIFICATION_PAYLOAD_ID, 0)
13 | val notificationType = intent.getStringExtra(AndroidNotificationService.NOTIFICATION_PAYLOAD_TYPE)
14 | val notification = intent.getParcelableExtra(AndroidNotificationService.NOTIFICATION_PAYLOAD_NOTIFICATION)
15 |
16 | with(context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) {
17 | if (notificationType == AndroidNotificationService.NOTIFICATION_TYPE_DISMISS) {
18 | cancel(notificationId)
19 | } else {
20 | notify(notificationId, notification)
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/service/NotificationRescheduler.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.service
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import co.touchlab.droidcon.application.service.NotificationSchedulingService
7 | import kotlinx.coroutines.MainScope
8 | import kotlinx.coroutines.launch
9 | import org.koin.core.component.KoinComponent
10 | import org.koin.core.component.inject
11 |
12 | class NotificationRescheduler :
13 | BroadcastReceiver(),
14 | KoinComponent {
15 | private val notificationSchedulingService by inject()
16 |
17 | override fun onReceive(context: Context?, intent: Intent?) {
18 | val backgroundIntent = goAsync()
19 | MainScope().launch {
20 | try {
21 | notificationSchedulingService.rescheduleAll()
22 | } finally {
23 | backgroundIntent.finish()
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/AssetResourceReader.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | import android.content.Context
4 | import co.touchlab.droidcon.domain.service.impl.ResourceReader
5 | import java.io.InputStreamReader
6 |
7 | class AssetResourceReader(private val context: Context) : ResourceReader {
8 | override fun readResource(name: String): String {
9 | // TODO: Catch Android-only exceptions and map them to common ones.
10 | return context.assets.open(name).use { stream ->
11 | InputStreamReader(stream).use { reader ->
12 | reader.readText()
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/ClasspathResourceReader.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | import co.touchlab.droidcon.domain.service.impl.ResourceReader
4 | import java.io.InputStreamReader
5 |
6 | class ClasspathResourceReader : ResourceReader {
7 | override fun readResource(name: String): String {
8 | // TODO: Catch Android-only exceptions and map them to common ones.
9 | return javaClass.classLoader?.getResourceAsStream(name).use { stream ->
10 | InputStreamReader(stream).use { reader ->
11 | reader.readText()
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/IdentifiableIntent.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 |
6 | class IdentifiableIntent(private val id: String, packageContext: Context, cls: Class<*>) : Intent(packageContext, cls) {
7 |
8 | override fun filterEquals(other: Intent?): Boolean {
9 | if (this === other) return true
10 | if (javaClass != other?.javaClass) return false
11 | if (this.id != (other as IdentifiableIntent).id) return false
12 | return true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/Platform.android.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | internal actual fun printThrowable(t: Throwable) {
4 | t.printStackTrace()
5 | }
6 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util.formatter
2 |
3 | import co.touchlab.droidcon.domain.service.ConferenceConfigProvider
4 | import co.touchlab.droidcon.domain.service.DateTimeService
5 | import java.text.DateFormat
6 | import java.text.SimpleDateFormat
7 | import java.util.Date
8 | import java.util.Locale
9 | import kotlinx.datetime.LocalDate
10 | import kotlinx.datetime.LocalDateTime
11 | import kotlinx.datetime.atTime
12 |
13 | class AndroidDateFormatter(private val dateTimeService: DateTimeService, private val conferenceConfigProvider: ConferenceConfigProvider) :
14 | DateFormatter {
15 |
16 | // Get timezone from ConferenceConfigProvider
17 | private val conferenceTimeZone get() = conferenceConfigProvider.getConferenceTimeZone()
18 |
19 | // Create formatters as properties to ensure they use the current conference timezone
20 | private val shortDateFormat
21 | get() = SimpleDateFormat("MMM d", Locale.getDefault()).apply {
22 | timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone.id)
23 | }
24 |
25 | private val minuteHourTimeFormat
26 | get() = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault())
27 | .apply { timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone.id) }
28 |
29 | override fun monthWithDay(date: LocalDate): String = shortDateFormat.format(
30 | Date(with(dateTimeService) { date.atTime(0, 0).fromConferenceDateTime(conferenceTimeZone) }.toEpochMilliseconds()),
31 | ).uppercase()
32 |
33 | override fun timeOnly(dateTime: LocalDateTime): String? = minuteHourTimeFormat.format(
34 | Date(with(dateTimeService) { dateTime.fromConferenceDateTime(conferenceTimeZone) }.toEpochMilliseconds()),
35 | )
36 |
37 | override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String =
38 | timeOnly(fromDateTime) + " - " + timeOnly(toDateTime)
39 | }
40 |
--------------------------------------------------------------------------------
/shared/src/androidMain/res/drawable/ic_baseline_insert_invitation_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/shared/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Notifications
3 | Reminders and feedback.
4 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/composite/AboutItem.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.composite
2 |
3 | data class AboutItem(val icon: String, val title: String, val detail: String)
4 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/composite/Settings.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.composite
2 |
3 | data class Settings(val isFeedbackEnabled: Boolean, val isRemindersEnabled: Boolean, val isFirstRun: Boolean = true)
4 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/gateway/SettingsGateway.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.gateway
2 |
3 | import co.touchlab.droidcon.application.composite.Settings
4 | import kotlinx.coroutines.flow.StateFlow
5 |
6 | interface SettingsGateway {
7 |
8 | fun settings(): StateFlow
9 |
10 | suspend fun setFeedbackEnabled(enabled: Boolean)
11 |
12 | suspend fun setRemindersEnabled(enabled: Boolean)
13 |
14 | suspend fun setFirstRun(isFirstRun: Boolean)
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/gateway/impl/DefaultSettingsGateway.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.gateway.impl
2 |
3 | import co.touchlab.droidcon.application.composite.Settings
4 | import co.touchlab.droidcon.application.gateway.SettingsGateway
5 | import co.touchlab.droidcon.application.repository.SettingsRepository
6 | import kotlinx.coroutines.flow.StateFlow
7 |
8 | class DefaultSettingsGateway(private val settingsRepository: SettingsRepository) : SettingsGateway {
9 |
10 | override fun settings(): StateFlow = settingsRepository.settings
11 |
12 | override suspend fun setFeedbackEnabled(enabled: Boolean) {
13 | settingsRepository.setFeedbackEnabled(enabled)
14 | }
15 |
16 | override suspend fun setRemindersEnabled(enabled: Boolean) {
17 | settingsRepository.setRemindersEnabled(enabled)
18 | }
19 |
20 | override suspend fun setFirstRun(isFirstRun: Boolean) {
21 | settingsRepository.setFirstRun(isFirstRun)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/repository/AboutRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.repository
2 |
3 | import co.touchlab.droidcon.application.composite.AboutItem
4 |
5 | interface AboutRepository {
6 |
7 | suspend fun getAboutItems(): List
8 | }
9 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/repository/SettingsRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.repository
2 |
3 | import co.touchlab.droidcon.application.composite.Settings
4 | import kotlinx.coroutines.flow.StateFlow
5 |
6 | interface SettingsRepository {
7 |
8 | val settings: StateFlow
9 |
10 | suspend fun set(settings: Settings)
11 |
12 | suspend fun setFeedbackEnabled(enabled: Boolean)
13 |
14 | suspend fun setRemindersEnabled(enabled: Boolean)
15 |
16 | suspend fun setFirstRun(isFirstRun: Boolean)
17 | }
18 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/repository/impl/DefaultAboutRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.repository.impl
2 |
3 | import co.touchlab.droidcon.application.composite.AboutItem
4 | import co.touchlab.droidcon.application.repository.AboutRepository
5 | import co.touchlab.droidcon.domain.service.impl.json.AboutJsonResourceDataSource
6 |
7 | class DefaultAboutRepository(private val aboutJsonResourceDataSource: AboutJsonResourceDataSource) : AboutRepository {
8 |
9 | override suspend fun getAboutItems(): List = aboutJsonResourceDataSource.getAboutItems().map {
10 | AboutItem(
11 | icon = it.icon,
12 | title = it.title,
13 | detail = it.detail,
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/Notification.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.service
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 |
5 | sealed interface Notification {
6 | sealed interface DeepLink : Notification
7 |
8 | sealed interface Local : Notification {
9 | data class Reminder(val sessionId: Session.Id) :
10 | Local,
11 | DeepLink
12 |
13 | data class Feedback(val sessionId: Session.Id) :
14 | Local,
15 | DeepLink
16 | }
17 |
18 | sealed interface Remote : Notification {
19 | data object RefreshData : Remote
20 | }
21 |
22 | object Keys {
23 | const val NOTIFICATION_TYPE = "notification_type"
24 | const val SESSION_ID = "session_id"
25 | }
26 |
27 | object Values {
28 | const val REMINDER_TYPE = "reminder"
29 | const val FEEDBACK_TYPE = "feedback"
30 | const val REFRESH_DATA_TYPE = "refresh_data"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationSchedulingService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.service
2 |
3 | interface NotificationSchedulingService {
4 | companion object {
5 | // MARK: Delivery offsets (in minutes)
6 | const val REMINDER_DELIVERY_START_OFFSET: Long = -10
7 | const val REMINDER_DISMISS_OFFSET: Long = 20
8 | const val FEEDBACK_DISMISS_END_OFFSET: Long = 10
9 | }
10 |
11 | suspend fun runScheduling()
12 |
13 | suspend fun rescheduleAll()
14 |
15 | interface LocalizedStringFactory {
16 |
17 | fun reminderTitle(roomName: String?): String
18 | fun reminderBody(sessionTitle: String): String
19 |
20 | fun feedbackTitle(): String
21 | fun feedbackBody(): String
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.application.service
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 | import co.touchlab.droidcon.service.DeepLinkNotificationHandler
5 | import kotlinx.datetime.Instant
6 |
7 | interface NotificationService {
8 | suspend fun initialize(): Boolean
9 |
10 | suspend fun schedule(notification: Notification.Local, title: String, body: String, delivery: Instant, dismiss: Instant?)
11 |
12 | suspend fun cancel(sessionIds: List)
13 |
14 | fun setHandler(notificationHandler: DeepLinkNotificationHandler)
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/composite/Url.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.composite
2 |
3 | data class Url(val string: String)
4 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/composite/ScheduleItem.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.composite
2 |
3 | import co.touchlab.droidcon.domain.entity.Profile
4 | import co.touchlab.droidcon.domain.entity.Room
5 | import co.touchlab.droidcon.domain.entity.Session
6 |
7 | data class ScheduleItem(val session: Session, val isInConflict: Boolean, val room: Room?, val speakers: List)
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/composite/SponsorGroupWithSponsors.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.composite
2 |
3 | import co.touchlab.droidcon.domain.entity.Sponsor
4 | import co.touchlab.droidcon.domain.entity.SponsorGroup
5 |
6 | data class SponsorGroupWithSponsors(val group: SponsorGroup, val sponsors: List)
7 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Conference.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.entity
2 |
3 | import kotlinx.datetime.TimeZone
4 |
5 | data class Conference(
6 | private val _id: Long? = null,
7 | val name: String,
8 | val timeZone: TimeZone,
9 | val projectId: String,
10 | val collectionName: String,
11 | val apiKey: String,
12 | val scheduleId: String,
13 | val selected: Boolean = false,
14 | val active: Boolean = true,
15 | ) : DomainEntity() {
16 | val showVenueMap: Boolean = true // We'll need to add this to the table
17 | override val id: Long
18 | get() = requireNotNull(_id) { "Conference id cannot be null" }
19 | }
20 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/DomainEntity.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.entity
2 |
3 | abstract class DomainEntity {
4 | abstract val id: ID
5 |
6 | override fun hashCode(): Int = id.hashCode()
7 |
8 | override fun equals(other: Any?): Boolean {
9 | if (this === other) return true
10 | if (other == null || this::class != other::class) return false
11 |
12 | other as DomainEntity<*>
13 |
14 | if (id != other.id) return false
15 |
16 | return true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Profile.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.entity
2 |
3 | import co.touchlab.droidcon.composite.Url
4 |
5 | // TODO: Add sponsors if desired.
6 | class Profile(
7 | override val id: Id,
8 | val fullName: String,
9 | val bio: String?,
10 | val tagLine: String?,
11 | val profilePicture: Url?,
12 | val twitter: Url?,
13 | val linkedIn: Url?,
14 | val website: Url?,
15 | ) : DomainEntity() {
16 | data class Id(val value: String)
17 | }
18 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Room.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.entity
2 |
3 | class Room(override val id: Id, val name: String) : DomainEntity() {
4 | data class Id(val value: Long)
5 | }
6 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Session.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.entity
2 |
3 | import co.touchlab.droidcon.domain.service.DateTimeService
4 | import kotlinx.datetime.Instant
5 |
6 | class Session(
7 | private val dateTimeService: DateTimeService,
8 | override val id: Id,
9 | val title: String,
10 | val description: String?,
11 | val startsAt: Instant,
12 | val endsAt: Instant,
13 | val isServiceSession: Boolean,
14 | val room: Room.Id?,
15 | var rsvp: RSVP,
16 | var feedback: Feedback?,
17 | ) : DomainEntity() {
18 |
19 | val isAttendable: Boolean
20 | get() = dateTimeService.now() < endsAt
21 |
22 | data class Id(val value: String)
23 |
24 | data class RSVP(val isAttending: Boolean, val isSent: Boolean)
25 |
26 | data class Feedback(val rating: Int, val comment: String, val isSent: Boolean) {
27 | object Rating {
28 | const val DISSATISFIED = 1
29 | const val NORMAL = 2
30 | const val SATISFIED = 3
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Sponsor.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.entity
2 |
3 | import co.touchlab.droidcon.composite.Url
4 |
5 | class Sponsor(override val id: Id, val hasDetail: Boolean, val description: String?, val icon: Url, val url: Url) :
6 | DomainEntity() {
7 |
8 | val name: String
9 | get() = id.name
10 |
11 | val group: String
12 | get() = id.group
13 |
14 | data class Id(val name: String, val group: String)
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/SponsorGroup.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.entity
2 |
3 | class SponsorGroup(override val id: Id, val displayPriority: Int, val isProminent: Boolean) : DomainEntity() {
4 | val name: String
5 | get() = id.value
6 |
7 | data class Id(val value: String)
8 | }
9 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/SessionGateway.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.gateway
2 |
3 | import co.touchlab.droidcon.domain.composite.ScheduleItem
4 | import co.touchlab.droidcon.domain.entity.Session
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface SessionGateway {
8 |
9 | fun observeSchedule(): Flow>
10 |
11 | fun observeAgenda(): Flow>
12 |
13 | fun observeScheduleItem(id: Session.Id): Flow
14 |
15 | suspend fun setAttending(session: Session, attending: Boolean)
16 |
17 | suspend fun setFeedback(session: Session, feedback: Session.Feedback)
18 |
19 | suspend fun getScheduleItem(id: Session.Id): ScheduleItem?
20 | }
21 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/SponsorGateway.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.gateway
2 |
3 | import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors
4 | import co.touchlab.droidcon.domain.entity.Profile
5 | import co.touchlab.droidcon.domain.entity.Sponsor
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | interface SponsorGateway {
9 |
10 | fun observeSponsors(): Flow>
11 |
12 | fun observeSponsorById(id: Sponsor.Id): Flow
13 |
14 | suspend fun getRepresentatives(sponsorId: Sponsor.Id): List
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.gateway.impl
2 |
3 | import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors
4 | import co.touchlab.droidcon.domain.entity.Profile
5 | import co.touchlab.droidcon.domain.entity.Sponsor
6 | import co.touchlab.droidcon.domain.gateway.SponsorGateway
7 | import co.touchlab.droidcon.domain.repository.ProfileRepository
8 | import co.touchlab.droidcon.domain.repository.SponsorGroupRepository
9 | import co.touchlab.droidcon.domain.repository.SponsorRepository
10 | import co.touchlab.droidcon.domain.service.ConferenceConfigProvider
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.map
13 |
14 | class DefaultSponsorGateway(
15 | private val sponsorRepository: SponsorRepository,
16 | private val sponsorGroupRepository: SponsorGroupRepository,
17 | private val profileRepository: ProfileRepository,
18 | private val conferenceConfigProvider: ConferenceConfigProvider,
19 | ) : SponsorGateway {
20 |
21 | override fun observeSponsors(): Flow> =
22 | sponsorGroupRepository.observeAll(conferenceConfigProvider.getConferenceId()).map { groups ->
23 | groups.map { group ->
24 | SponsorGroupWithSponsors(
25 | group,
26 | sponsorRepository.allByGroupName(group.name, conferenceConfigProvider.getConferenceId()),
27 | )
28 | }
29 | }
30 |
31 | override fun observeSponsorById(id: Sponsor.Id): Flow =
32 | sponsorRepository.observe(id, conferenceConfigProvider.getConferenceId())
33 |
34 | override suspend fun getRepresentatives(sponsorId: Sponsor.Id): List =
35 | profileRepository.getSponsorRepresentatives(sponsorId, conferenceConfigProvider.getConferenceId())
36 | }
37 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ConferenceRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository
2 |
3 | import co.touchlab.droidcon.domain.entity.Conference
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface ConferenceRepository {
7 | /**
8 | * Observes all active conferences
9 | */
10 | fun observeAll(): Flow>
11 |
12 | fun observeSelected(): Flow
13 |
14 | suspend fun getSelected(): Conference
15 |
16 | suspend fun select(conferenceId: Long): Boolean
17 |
18 | suspend fun add(conference: Conference): Long
19 |
20 | suspend fun update(conference: Conference): Boolean
21 |
22 | suspend fun delete(conferenceId: Long): Boolean
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository
2 |
3 | import co.touchlab.droidcon.domain.entity.Profile
4 | import co.touchlab.droidcon.domain.entity.Session
5 | import co.touchlab.droidcon.domain.entity.Sponsor
6 |
7 | interface ProfileRepository : Repository {
8 |
9 | suspend fun getSpeakersBySession(id: Session.Id, conferenceId: Long): List
10 |
11 | fun setSessionSpeakers(session: Session, speakers: List, conferenceId: Long)
12 |
13 | fun setSponsorRepresentatives(sponsor: Sponsor, representatives: List, conferenceId: Long)
14 |
15 | suspend fun getSponsorRepresentatives(sponsorId: Sponsor.Id, conferenceId: Long): List
16 |
17 | fun allSync(conferenceId: Long): List
18 | }
19 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository
2 |
3 | import co.touchlab.droidcon.domain.entity.DomainEntity
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface Repository> {
7 | suspend fun get(id: ID, conferenceId: Long): ENTITY
8 |
9 | suspend fun find(id: ID, conferenceId: Long): ENTITY?
10 |
11 | fun observe(id: ID, conferenceId: Long): Flow
12 |
13 | fun observeOrNull(id: ID, conferenceId: Long): Flow
14 |
15 | fun observe(entity: ENTITY, conferenceId: Long): Flow
16 |
17 | suspend fun all(conferenceId: Long): List
18 |
19 | fun observeAll(conferenceId: Long): Flow>
20 |
21 | fun add(entity: ENTITY, conferenceId: Long)
22 |
23 | fun remove(entity: ENTITY, conferenceId: Long): Boolean
24 |
25 | fun remove(id: ID, conferenceId: Long): Boolean
26 |
27 | fun update(entity: ENTITY, conferenceId: Long)
28 |
29 | fun addOrUpdate(entity: ENTITY, conferenceId: Long)
30 |
31 | fun contains(id: ID, conferenceId: Long): Boolean
32 | }
33 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/RoomRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository
2 |
3 | import co.touchlab.droidcon.domain.entity.Room
4 |
5 | interface RoomRepository : Repository {
6 | fun allSync(conferenceId: Long): List
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SessionRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface SessionRepository : Repository {
7 |
8 | fun observeAllAttending(conferenceId: Long): Flow>
9 |
10 | suspend fun allAttending(conferenceId: Long): List
11 |
12 | suspend fun setRsvp(sessionId: Session.Id, rsvp: Session.RSVP, conferenceId: Long)
13 |
14 | suspend fun setRsvpSent(sessionId: Session.Id, isSent: Boolean, conferenceId: Long)
15 |
16 | suspend fun setFeedback(sessionId: Session.Id, feedback: Session.Feedback, conferenceId: Long)
17 |
18 | suspend fun setFeedbackSent(sessionId: Session.Id, isSent: Boolean, conferenceId: Long)
19 |
20 | fun allSync(conferenceId: Long): List
21 |
22 | fun findSync(id: Session.Id, conferenceId: Long): Session?
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository
2 |
3 | import co.touchlab.droidcon.domain.entity.SponsorGroup
4 |
5 | interface SponsorGroupRepository : Repository {
6 | fun allSync(conferenceId: Long): List
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository
2 |
3 | import co.touchlab.droidcon.domain.entity.Sponsor
4 |
5 | interface SponsorRepository : Repository {
6 | suspend fun allByGroupName(group: String, conferenceId: Long): List
7 |
8 | fun allSync(conferenceId: Long): List
9 | }
10 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/BaseRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository.impl
2 |
3 | import co.touchlab.droidcon.domain.entity.DomainEntity
4 | import co.touchlab.droidcon.domain.repository.Repository
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.first
7 |
8 | abstract class BaseRepository> : Repository {
9 | override suspend fun get(id: ID, conferenceId: Long): ENTITY = observe(id, conferenceId).first()
10 |
11 | override suspend fun find(id: ID, conferenceId: Long): ENTITY? = observeOrNull(id, conferenceId).first()
12 |
13 | override fun observe(entity: ENTITY, conferenceId: Long): Flow = observe(entity.id, conferenceId)
14 |
15 | override suspend fun all(conferenceId: Long): List = observeAll(conferenceId).first()
16 |
17 | override fun add(entity: ENTITY, conferenceId: Long) {
18 | if (!contains(entity.id, conferenceId)) {
19 | doUpsert(entity, conferenceId)
20 | } else {
21 | // TODO: Throw custom repository exception
22 | error("Can't insert entity: $entity which already exist in the database.")
23 | }
24 | }
25 |
26 | override fun remove(entity: ENTITY, conferenceId: Long) = remove(entity.id, conferenceId)
27 |
28 | override fun remove(id: ID, conferenceId: Long): Boolean {
29 | val idExists = contains(id, conferenceId)
30 | if (idExists) {
31 | doDelete(id, conferenceId)
32 | }
33 | return idExists
34 | }
35 |
36 | override fun update(entity: ENTITY, conferenceId: Long) {
37 | if (contains(entity.id, conferenceId)) {
38 | doUpsert(entity, conferenceId)
39 | } else {
40 | // TODO: Throw custom repository exception
41 | error("Can't update entity: $entity which doesn't exist in the database.")
42 | }
43 | }
44 |
45 | override fun addOrUpdate(entity: ENTITY, conferenceId: Long) = doUpsert(entity, conferenceId)
46 |
47 | protected abstract fun doUpsert(entity: ENTITY, conferenceId: Long)
48 |
49 | protected abstract fun doDelete(id: ID, conferenceId: Long)
50 |
51 | protected fun Long.toBoolean(): Boolean = this != 0L
52 |
53 | protected fun Boolean.toLong(): Long = if (this) {
54 | 1L
55 | } else {
56 | 0L
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository.impl
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 |
5 | expect class SqlDelightDriverFactory {
6 | fun createDriver(): SqlDriver
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository.impl
2 |
3 | import app.cash.sqldelight.coroutines.asFlow
4 | import app.cash.sqldelight.coroutines.mapToList
5 | import app.cash.sqldelight.coroutines.mapToOne
6 | import app.cash.sqldelight.coroutines.mapToOneOrNull
7 | import co.touchlab.droidcon.db.RoomQueries
8 | import co.touchlab.droidcon.domain.entity.Room
9 | import co.touchlab.droidcon.domain.repository.RoomRepository
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.flow.Flow
12 |
13 | class SqlDelightRoomRepository(private val roomQueries: RoomQueries) :
14 | BaseRepository(),
15 | RoomRepository {
16 |
17 | override fun allSync(conferenceId: Long): List = roomQueries.selectAll(conferenceId, ::roomFactory).executeAsList()
18 |
19 | override fun observe(id: Room.Id, conferenceId: Long): Flow =
20 | roomQueries.selectById(id.value, conferenceId, ::roomFactory).asFlow().mapToOne(Dispatchers.Main)
21 |
22 | override fun observeOrNull(id: Room.Id, conferenceId: Long): Flow =
23 | roomQueries.selectById(id.value, conferenceId, ::roomFactory).asFlow().mapToOneOrNull(Dispatchers.Main)
24 |
25 | override fun observeAll(conferenceId: Long): Flow> =
26 | roomQueries.selectAll(conferenceId, ::roomFactory).asFlow().mapToList(Dispatchers.Main)
27 |
28 | override fun doUpsert(entity: Room, conferenceId: Long) {
29 | roomQueries.upsert(
30 | id = entity.id.value,
31 | conferenceId = conferenceId,
32 | name = entity.name,
33 | )
34 | }
35 |
36 | override fun doDelete(id: Room.Id, conferenceId: Long) {
37 | roomQueries.deleteById(id.value, conferenceId)
38 | }
39 |
40 | override fun contains(id: Room.Id, conferenceId: Long): Boolean = roomQueries.existsById(id.value, conferenceId).executeAsOne() != 0L
41 |
42 | private fun roomFactory(id: Long, conferenceId: Long, name: String) = Room(id = Room.Id(id), name = name)
43 | }
44 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository.impl
2 |
3 | import app.cash.sqldelight.coroutines.asFlow
4 | import app.cash.sqldelight.coroutines.mapToList
5 | import app.cash.sqldelight.coroutines.mapToOne
6 | import app.cash.sqldelight.coroutines.mapToOneOrNull
7 | import co.touchlab.droidcon.db.SponsorGroupQueries
8 | import co.touchlab.droidcon.domain.entity.SponsorGroup
9 | import co.touchlab.droidcon.domain.repository.SponsorGroupRepository
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.flow.Flow
12 |
13 | class SqlDelightSponsorGroupRepository(private val sponsorGroupQueries: SponsorGroupQueries) :
14 | BaseRepository(),
15 | SponsorGroupRepository {
16 |
17 | override fun allSync(conferenceId: Long): List =
18 | sponsorGroupQueries.selectAll(conferenceId, ::sponsorGroupFactory).executeAsList()
19 |
20 | override fun observe(id: SponsorGroup.Id, conferenceId: Long): Flow =
21 | sponsorGroupQueries.sponsorGroupByName(id.value, conferenceId, ::sponsorGroupFactory)
22 | .asFlow().mapToOne(Dispatchers.Main)
23 |
24 | override fun observeOrNull(id: SponsorGroup.Id, conferenceId: Long): Flow =
25 | sponsorGroupQueries.sponsorGroupByName(id.value, conferenceId, ::sponsorGroupFactory)
26 | .asFlow().mapToOneOrNull(Dispatchers.Main)
27 |
28 | override fun observeAll(conferenceId: Long): Flow> =
29 | sponsorGroupQueries.selectAll(conferenceId, ::sponsorGroupFactory)
30 | .asFlow().mapToList(Dispatchers.Main)
31 |
32 | override fun contains(id: SponsorGroup.Id, conferenceId: Long): Boolean =
33 | sponsorGroupQueries.existsByName(id.value, conferenceId).executeAsOne().toBoolean()
34 |
35 | override fun doUpsert(entity: SponsorGroup, conferenceId: Long) {
36 | sponsorGroupQueries.upsert(
37 | name = entity.id.value,
38 | conferenceId = conferenceId,
39 | displayPriority = entity.displayPriority,
40 | prominent = entity.isProminent,
41 | )
42 | }
43 |
44 | override fun doDelete(id: SponsorGroup.Id, conferenceId: Long) {
45 | sponsorGroupQueries.deleteByName(id.value, conferenceId)
46 | }
47 |
48 | private fun sponsorGroupFactory(name: String, conferenceId: Long, displayPriority: Int, isProminent: Boolean) = SponsorGroup(
49 | id = SponsorGroup.Id(name),
50 | displayPriority = displayPriority,
51 | isProminent = isProminent,
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/adapter/InstantSqlDelightAdapter.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository.impl.adapter
2 |
3 | import app.cash.sqldelight.ColumnAdapter
4 | import kotlinx.datetime.Instant
5 |
6 | object InstantSqlDelightAdapter : ColumnAdapter {
7 |
8 | override fun decode(databaseValue: Long): Instant = Instant.fromEpochMilliseconds(databaseValue)
9 |
10 | override fun encode(value: Instant): Long = value.toEpochMilliseconds()
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AnalyticsService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service
2 |
3 | interface AnalyticsService {
4 |
5 | companion object {
6 |
7 | const val EVENT_STARTED: String = "STARTED"
8 | }
9 |
10 | fun logEvent(name: String, params: Map = emptyMap())
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service
2 |
3 | import co.touchlab.droidcon.domain.entity.Conference
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.datetime.TimeZone
6 |
7 | interface ConferenceConfigProvider {
8 | fun getConferenceId(): Long
9 | fun getConferenceTimeZone(): TimeZone
10 | fun getProjectId(): String
11 | fun getCollectionName(): String
12 | fun getApiKey(): String
13 | fun getScheduleId(): String
14 | fun showVenueMap(): Boolean
15 | fun observeChanges(): Flow
16 |
17 | /**
18 | * Get the currently selected conference
19 | */
20 | suspend fun getSelectedConference(): Conference
21 |
22 | /**
23 | * Initiates conference observation.
24 | */
25 | suspend fun loadSelectedConference()
26 | }
27 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/DateTimeService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service
2 |
3 | import kotlinx.datetime.Instant
4 | import kotlinx.datetime.LocalDateTime
5 | import kotlinx.datetime.TimeZone
6 |
7 | interface DateTimeService {
8 | fun now(): Instant
9 |
10 | fun conferenceNow(timeZone: TimeZone): LocalDateTime
11 |
12 | fun Instant.toConferenceDateTime(conferenceTimeZone: TimeZone): LocalDateTime
13 |
14 | fun LocalDateTime.fromConferenceDateTime(conferenceTimeZone: TimeZone): Instant
15 | }
16 |
17 | fun Instant.toConferenceDateTime(dateTimeService: DateTimeService, conferenceTimeZone: TimeZone): LocalDateTime = with(dateTimeService) {
18 | toConferenceDateTime(conferenceTimeZone)
19 | }
20 |
21 | fun LocalDateTime.fromConferenceDateTime(dateTimeService: DateTimeService, conferenceTimeZone: TimeZone): Instant = with(dateTimeService) {
22 | fromConferenceDateTime(conferenceTimeZone)
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/FeedbackService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 |
5 | interface FeedbackService {
6 | suspend fun next(): Session?
7 |
8 | suspend fun submit(session: Session, feedback: Session.Feedback)
9 |
10 | suspend fun skip(session: Session)
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ScheduleService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 |
5 | interface ScheduleService {
6 |
7 | suspend fun isInConflict(session: Session): Boolean
8 | }
9 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ServerApi.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 |
5 | interface ServerApi {
6 | suspend fun setRsvp(sessionId: Session.Id, isAttending: Boolean): Boolean
7 |
8 | suspend fun setFeedback(sessionId: Session.Id, rating: Int, comment: String): Boolean
9 | }
10 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/SyncService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service
2 |
3 | import co.touchlab.droidcon.domain.entity.Conference
4 |
5 | interface SyncService {
6 |
7 | suspend fun runSynchronization(conference: Conference)
8 |
9 | suspend fun forceSynchronize(conference: Conference): Boolean
10 |
11 | suspend fun syncConferences()
12 | }
13 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/UserIdProvider.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service
2 |
3 | interface UserIdProvider {
4 | suspend fun getId(): String
5 | }
6 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl
2 |
3 | import co.touchlab.droidcon.domain.entity.Conference
4 | import co.touchlab.droidcon.domain.repository.ConferenceRepository
5 | import co.touchlab.droidcon.domain.service.ConferenceConfigProvider
6 | import co.touchlab.kermit.Logger
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.StateFlow
10 | import kotlinx.datetime.TimeZone
11 |
12 | class DefaultConferenceConfigProvider(private val conferenceRepository: ConferenceRepository, initialConference: Conference) :
13 | ConferenceConfigProvider {
14 | private val log = Logger.withTag("DefaultConferenceConfigProvider")
15 | private val _currentConferenceState = MutableStateFlow(initialConference)
16 | val currentConferenceState: StateFlow = _currentConferenceState
17 |
18 | private val currentConference: Conference
19 | get() = currentConferenceState.value
20 |
21 | override fun getConferenceId(): Long = currentConference.id
22 |
23 | override fun getConferenceTimeZone(): TimeZone = currentConference.timeZone
24 |
25 | override fun getProjectId(): String = "droidcon-148cc"
26 |
27 | override fun getCollectionName(): String = currentConference.collectionName
28 |
29 | override fun getApiKey(): String = currentConference.apiKey
30 |
31 | override fun getScheduleId(): String = currentConference.scheduleId
32 |
33 | override fun showVenueMap(): Boolean = true // Default to true, will be configurable per conference later
34 |
35 | override fun observeChanges(): Flow = conferenceRepository.observeSelected()
36 |
37 | // Implementation of the interface method to get the currently selected conference
38 | override suspend fun getSelectedConference(): Conference = conferenceRepository.getSelected()
39 |
40 | // Implementation of the interface method to load the conference asynchronously
41 | // Also sets up continuous observation of conference changes
42 | override suspend fun loadSelectedConference() {
43 | conferenceRepository.observeSelected().collect { conference ->
44 | _currentConferenceState.emit(conference)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultDateTimeService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl
2 |
3 | import co.touchlab.droidcon.domain.service.DateTimeService
4 | import kotlinx.datetime.Clock
5 | import kotlinx.datetime.Instant
6 | import kotlinx.datetime.LocalDateTime
7 | import kotlinx.datetime.TimeZone
8 | import kotlinx.datetime.toInstant
9 | import kotlinx.datetime.toLocalDateTime
10 |
11 | class DefaultDateTimeService(private val clock: Clock) : DateTimeService {
12 |
13 | override fun now(): Instant = clock.now()
14 |
15 | override fun conferenceNow(timeZone: TimeZone): LocalDateTime = now().toConferenceDateTime(timeZone)
16 |
17 | override fun Instant.toConferenceDateTime(conferenceTimeZone: TimeZone): LocalDateTime = toLocalDateTime(conferenceTimeZone)
18 |
19 | override fun LocalDateTime.fromConferenceDateTime(conferenceTimeZone: TimeZone): Instant = toInstant(conferenceTimeZone)
20 | }
21 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 | import co.touchlab.droidcon.domain.gateway.SessionGateway
5 | import co.touchlab.droidcon.domain.service.FeedbackService
6 | import com.russhwolf.settings.ExperimentalSettingsApi
7 | import com.russhwolf.settings.ObservableSettings
8 | import com.russhwolf.settings.set
9 | import kotlinx.coroutines.flow.first
10 | import kotlinx.datetime.Clock
11 | import kotlinx.serialization.encodeToString
12 | import kotlinx.serialization.json.Json
13 |
14 | @OptIn(ExperimentalSettingsApi::class)
15 | class DefaultFeedbackService(
16 | private val sessionGateway: SessionGateway,
17 | private val settings: ObservableSettings,
18 | private val json: Json,
19 | private val clock: Clock,
20 | ) : FeedbackService {
21 | companion object {
22 | private const val COMPLETED_SESSION_FEEDBACKS_KEY = "COMPLETED_SESSION_FEEDBACKS"
23 | }
24 |
25 | private var completedSessionIds: Set = settings.getStringOrNull(COMPLETED_SESSION_FEEDBACKS_KEY)?.let {
26 | json.decodeFromString(it)
27 | } ?: emptySet()
28 |
29 | override suspend fun next(): Session? = sessionGateway.observeAgenda().first()
30 | .firstOrNull { it.session.endsAt < clock.now() && !completedSessionIds.contains(it.session.id.value) }
31 | ?.session
32 |
33 | override suspend fun submit(session: Session, feedback: Session.Feedback) {
34 | sessionGateway.setFeedback(session, feedback)
35 | completedSessionIds += session.id.value
36 | saveCompletedSessions()
37 | }
38 |
39 | override suspend fun skip(session: Session) {
40 | completedSessionIds += session.id.value
41 | saveCompletedSessions()
42 | }
43 |
44 | private fun saveCompletedSessions() {
45 | settings[COMPLETED_SESSION_FEEDBACKS_KEY] = json.encodeToString(completedSessionIds)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 | import co.touchlab.droidcon.domain.repository.SessionRepository
5 | import co.touchlab.droidcon.domain.service.ConferenceConfigProvider
6 | import co.touchlab.droidcon.domain.service.ScheduleService
7 | import kotlinx.datetime.Instant
8 |
9 | class DefaultScheduleService(
10 | private val sessionRepository: SessionRepository,
11 | private val conferenceConfigProvider: ConferenceConfigProvider,
12 | ) : ScheduleService {
13 |
14 | override suspend fun isInConflict(session: Session): Boolean {
15 | if (!session.rsvp.isAttending) {
16 | return false
17 | }
18 | val sessionRange = session.startsAt.rangeTo(session.endsAt)
19 | val conferenceId = conferenceConfigProvider.getConferenceId()
20 | return sessionRepository.allAttending(conferenceId).any { otherSession ->
21 | otherSession.id != session.id && sessionRange.intersects(otherSession.startsAt.rangeTo(otherSession.endsAt))
22 | }
23 | }
24 |
25 | private fun ClosedRange.intersects(otherRange: ClosedRange): Boolean =
26 | start.epochSeconds < otherRange.endInclusive.epochSeconds &&
27 | endInclusive.epochSeconds > otherRange.start.epochSeconds
28 | }
29 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultServerApi.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl
2 |
3 | import co.touchlab.droidcon.domain.entity.Session
4 | import co.touchlab.droidcon.domain.service.ServerApi
5 | import co.touchlab.droidcon.domain.service.UserIdProvider
6 | import io.ktor.client.HttpClient
7 | import io.ktor.client.request.HttpRequestBuilder
8 | import io.ktor.client.request.forms.submitForm
9 | import io.ktor.http.HttpMethod
10 | import io.ktor.http.Parameters
11 | import io.ktor.http.encodedPath
12 | import io.ktor.http.isSuccess
13 | import io.ktor.http.takeFrom
14 | import kotlinx.serialization.json.Json
15 |
16 | class DefaultServerApi(private val userIdProvider: UserIdProvider, private val client: HttpClient, private val json: Json) : ServerApi {
17 | override suspend fun setRsvp(sessionId: Session.Id, isAttending: Boolean): Boolean {
18 | val methodName = if (isAttending) {
19 | "sessionizeRsvpEvent"
20 | } else {
21 | "sessionizeUnrsvpEvent"
22 | }
23 |
24 | return client.submitForm {
25 | droidcon("/dataTest/$methodName/${sessionId.value}/${userIdProvider.getId()}")
26 | method = HttpMethod.Post
27 | }.status.isSuccess()
28 | }
29 |
30 | override suspend fun setFeedback(sessionId: Session.Id, rating: Int, comment: String): Boolean = client.submitForm(
31 | formParameters = Parameters.build {
32 | append("rating", rating.toString())
33 | append("comment", comment)
34 | },
35 | ) {
36 | droidcon("/dataTest/sessionizeFeedbackEvent/${sessionId.value}/${userIdProvider.getId()}")
37 | }.status.isSuccess()
38 |
39 | private fun HttpRequestBuilder.droidcon(path: String) {
40 | url {
41 | takeFrom("https://droidcon-server.herokuapp.com")
42 | encodedPath = path
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultUserIdProvider.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl
2 |
3 | import co.touchlab.droidcon.domain.service.UserIdProvider
4 | import com.benasher44.uuid.uuid4
5 | import com.russhwolf.settings.ExperimentalSettingsApi
6 | import com.russhwolf.settings.ObservableSettings
7 | import com.russhwolf.settings.get
8 | import com.russhwolf.settings.set
9 |
10 | @OptIn(ExperimentalSettingsApi::class)
11 | class DefaultUserIdProvider(private val observableSettings: ObservableSettings) : UserIdProvider {
12 | companion object {
13 | const val USER_ID_KEY = "USER_ID_KEY"
14 | }
15 |
16 | override suspend fun getId(): String {
17 | var id = observableSettings.get(USER_ID_KEY)
18 | if (id == null) {
19 | id = uuid4().toString()
20 | observableSettings[USER_ID_KEY] = id
21 | }
22 | return id
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/ResourceReader.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl
2 |
3 | interface ResourceReader {
4 | fun readResource(name: String): String
5 | }
6 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/dto/AboutDto.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | object AboutDto {
6 |
7 | @Serializable
8 | data class AboutItemDto(val icon: String, val title: String, val detail: String)
9 | }
10 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/dto/ConferencesDto.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | object ConferencesDto {
7 |
8 | @Serializable
9 | data class ConferenceCollectionDto(
10 | @SerialName("documents")
11 | val conferences: List,
12 | )
13 |
14 | @Serializable
15 | data class ConferenceDocumentDto(val name: String, val fields: ConferenceFields, val createTime: String, val updateTime: String)
16 |
17 | @Serializable
18 | data class ConferenceFields(
19 | val conferenceName: StringValue,
20 | val conferenceTimeZone: StringValue,
21 | val projectId: StringValue,
22 | val collectionName: StringValue,
23 | val apiKey: StringValue,
24 | val scheduleId: StringValue,
25 | )
26 |
27 | @Serializable
28 | data class StringValue(val stringValue: String)
29 | }
30 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/dto/ScheduleDto.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.json.JsonArray
6 |
7 | object ScheduleDto {
8 |
9 | @Serializable
10 | data class DayDto(val date: String, val rooms: List)
11 |
12 | @Serializable
13 | data class RoomDto(val id: Long, val name: String, val sessions: List)
14 |
15 | @Serializable
16 | data class SessionDto(
17 | val id: String,
18 | val title: String,
19 | val description: String? = null,
20 | // @Serializable(with = InstantIso8601Serializer::class)
21 | val startsAt: String,
22 | // @Serializable(with = InstantIso8601Serializer::class)
23 | val endsAt: String,
24 | val isServiceSession: Boolean,
25 | val isPlenumSession: Boolean,
26 | val speakers: List,
27 | val categories: JsonArray,
28 |
29 | @SerialName("roomId")
30 | val roomID: Long,
31 | val room: String,
32 | )
33 |
34 | @Serializable
35 | data class SpeakerDto(val id: String, val name: String)
36 |
37 | fun List.sessions(): List = flatMap { day ->
38 | day.rooms.flatMap { room ->
39 | room.sessions
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/dto/SpeakersDto.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl.dto
2 |
3 | import kotlinx.serialization.KSerializer
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.descriptors.PrimitiveKind
6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 | import kotlinx.serialization.json.JsonArray
11 |
12 | object SpeakersDto {
13 |
14 | @Serializable
15 | data class SpeakerDto(
16 | val id: String,
17 | val firstName: String,
18 | val lastName: String,
19 | val fullName: String,
20 | val bio: String?,
21 | val tagLine: String?,
22 | val profilePicture: String?,
23 | val sessions: List,
24 | val isTopSpeaker: Boolean,
25 | val links: List,
26 | val questionAnswers: JsonArray,
27 | val categories: JsonArray,
28 | )
29 |
30 | @Serializable
31 | data class LinkDto(val title: String, val url: String, val linkType: LinkType)
32 |
33 | @Serializable(with = LinkType.Companion::class)
34 | data class LinkType(val value: String) {
35 | companion object : KSerializer {
36 | val Blog = LinkType("Blog")
37 | val CompanyWebsite = LinkType("Company_Website")
38 | val LinkedIn = LinkType("LinkedIn")
39 | val Other = LinkType("Other")
40 | val Twitter = LinkType("Twitter")
41 |
42 | private val allTypes = listOf(
43 | Blog,
44 | CompanyWebsite,
45 | LinkedIn,
46 | Other,
47 | Twitter,
48 | ).associateBy { it.value }
49 |
50 | override val descriptor: SerialDescriptor
51 | get() {
52 | return PrimitiveSerialDescriptor("co.touchlab.droidcon.domain.service.impl.dto.LinkType", PrimitiveKind.STRING)
53 | }
54 |
55 | override fun deserialize(decoder: Decoder): LinkType = decoder.decodeString().let {
56 | allTypes[it] ?: LinkType(it)
57 | }
58 |
59 | override fun serialize(encoder: Encoder, value: LinkType) {
60 | encoder.encodeString(value.value)
61 | }
62 | }
63 | }
64 |
65 | @Serializable
66 | data class SessionDto(val id: Long, val name: String)
67 | }
68 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/dto/SponsorSessionsDto.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | object SponsorSessionsDto {
6 |
7 | @Serializable
8 | data class SessionGroupDto(val sessions: List)
9 |
10 | @Serializable
11 | data class SessionDto(val id: String, val title: String, val description: String?, val speakers: List)
12 |
13 | @Serializable
14 | data class SpeakerReferenceDto(val id: String, val name: String)
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/dto/SponsorsDto.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | object SponsorsDto {
7 |
8 | @Serializable
9 | data class SponsorCollectionDto(
10 | @SerialName("documents")
11 | val groups: List,
12 | )
13 |
14 | @Serializable
15 | data class SponsorGroupDto(val name: String, val fields: DocumentFields, val createTime: String, val updateTime: String)
16 |
17 | @Serializable
18 | data class DocumentFields(val displayOrder: DisplayOrder, val sponsors: Sponsors, val prominent: BooleanValue? = null)
19 |
20 | @Serializable
21 | data class DisplayOrder(val integerValue: String)
22 |
23 | @Serializable
24 | data class Sponsors(val arrayValue: ArrayValue)
25 |
26 | @Serializable
27 | data class ArrayValue(val values: List)
28 |
29 | @Serializable
30 | data class Value(val mapValue: MapValue)
31 |
32 | @Serializable
33 | data class MapValue(val fields: MapValueFields)
34 |
35 | @Serializable
36 | data class MapValueFields(val sponsorId: StringValue? = null, val name: StringValue, val icon: StringValue, val url: StringValue)
37 |
38 | @Serializable
39 | data class StringValue(val stringValue: String)
40 |
41 | @Serializable
42 | data class BooleanValue(val booleanValue: Boolean)
43 | }
44 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/AboutJsonResourceDataSource.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl.json
2 |
3 | import co.touchlab.droidcon.domain.service.impl.dto.AboutDto
4 | import kotlinx.serialization.builtins.ListSerializer
5 |
6 | class AboutJsonResourceDataSource(private val jsonResourceReader: JsonResourceReader) {
7 |
8 | fun getAboutItems(): List =
9 | jsonResourceReader.readAndDecodeResource("about.json", ListSerializer(AboutDto.AboutItemDto.serializer()))
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/JsonResourceReader.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.service.impl.json
2 |
3 | import co.touchlab.droidcon.domain.service.impl.ResourceReader
4 | import kotlinx.serialization.DeserializationStrategy
5 | import kotlinx.serialization.json.Json
6 |
7 | class JsonResourceReader(private val resourceReader: ResourceReader, private val json: Json) {
8 | internal fun readAndDecodeResource(name: String, strategy: DeserializationStrategy): T {
9 | val text = resourceReader.readResource(name)
10 | return json.decodeFromString(strategy, text)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/dto/WebLink.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.dto
2 |
3 | import co.touchlab.droidcon.composite.Url
4 |
5 | data class WebLink(val range: IntRange, val link: String) {
6 |
7 | companion object {
8 |
9 | fun fromUrl(url: Url): WebLink = WebLink(IntRange(0, url.string.length - 1), url.string)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/service/DeepLinkNotificationHandler.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.service
2 |
3 | import co.touchlab.droidcon.application.service.Notification
4 |
5 | interface DeepLinkNotificationHandler {
6 | fun handleDeepLinkNotification(notification: Notification.DeepLink)
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/service/ParseUrlViewService.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.service
2 |
3 | import co.touchlab.droidcon.dto.WebLink
4 |
5 | interface ParseUrlViewService {
6 |
7 | fun parse(text: String): List
8 | }
9 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/AppChecker.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | import co.touchlab.droidcon.domain.service.ConferenceConfigProvider
4 |
5 | /**
6 | * Utility class for app-specific checks.
7 | * Previously contained time zone hash checking, which has been removed.
8 | * Keeping the class for potential future app verification checks.
9 | */
10 | class AppChecker(private val conferenceConfigProvider: ConferenceConfigProvider)
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/Platform.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | internal expect fun printThrowable(t: Throwable)
4 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/formatter/DateFormatter.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util.formatter
2 |
3 | import kotlinx.datetime.LocalDate
4 | import kotlinx.datetime.LocalDateTime
5 |
6 | interface DateFormatter {
7 |
8 | fun monthWithDay(date: LocalDate): String?
9 | fun timeOnly(dateTime: LocalDateTime): String?
10 | fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/about.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "icon": "about_touchlab",
4 | "title": "About Touchlab",
5 | "detail": "Touchlab Accelerates Mobile Innovation for Enterprise and Startups.\n\nGet in touch.\n\nhttps://touchlab.co"
6 | },
7 | {
8 | "icon": "about_kotlin",
9 | "title": "Droidcon App",
10 | "detail": "The Droidcon app is a Kotlin Multiplatform app by the Touchlab team and some folks from the mobile development community.\n\nSee the repo and read more at:\nhttps://github.com/touchlab/DroidconKotlin"
11 | },
12 | {
13 | "icon": "about_droidcon",
14 | "title": "Droidcon",
15 | "detail": "Droidcon conferences around the world support the Android platform and create a global network for developers and companies.\n\nWe offer best-in-class presentations from leaders in all parts of the Android ecosystem, including core development, embedded solutions, augmented reality, business solutions, and games."
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/Profile.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE profileTable(
2 | id TEXT NOT NULL,
3 | conferenceId INTEGER NOT NULL,
4 | fullName TEXT NOT NULL,
5 | bio TEXT,
6 | tagLine TEXT,
7 | profilePicture TEXT,
8 | twitter TEXT,
9 | linkedIn TEXT,
10 | website TEXT,
11 | PRIMARY KEY (id, conferenceId),
12 | FOREIGN KEY (conferenceId) REFERENCES conferenceTable(id)
13 | );
14 |
15 | upsert:
16 | INSERT OR REPLACE INTO profileTable(id, conferenceId, fullName, bio, tagLine, profilePicture, twitter, linkedIn, website)
17 | VALUES(?,?,?,?,?,?,?,?,?);
18 |
19 | delete {
20 | DELETE FROM sessionSpeakerTable WHERE speakerId = :speakerId;
21 | DELETE FROM profileTable WHERE id = :speakerId AND conferenceId = :conferenceId;
22 | }
23 |
24 | selectById:
25 | SELECT * FROM profileTable WHERE id = ? AND conferenceId = ?;
26 |
27 | existsById:
28 | SELECT count(1) FROM profileTable WHERE id = ? AND conferenceId = ?;
29 |
30 | selectBySession:
31 | SELECT profileTable.*
32 | FROM profileTable
33 | JOIN sessionSpeakerTable ON sessionSpeakerTable.speakerId = id AND sessionSpeakerTable.conferenceId = profileTable.conferenceId
34 | WHERE sessionSpeakerTable.sessionId = ? AND profileTable.conferenceId = ?
35 | ORDER BY sessionSpeakerTable.displayOrder;
36 |
37 | selectBySponsor:
38 | SELECT profileTable.*
39 | FROM profileTable
40 | JOIN sponsorRepresentativeTable ON sponsorRepresentativeTable.representativeId = id AND sponsorRepresentativeTable.conferenceId = profileTable.conferenceId
41 | WHERE sponsorRepresentativeTable.sponsorName = ? AND sponsorRepresentativeTable.sponsorGroupName = ? AND profileTable.conferenceId = ?
42 | ORDER BY sponsorRepresentativeTable.displayOrder;
43 |
44 | selectAll:
45 | SELECT *
46 | FROM profileTable
47 | WHERE conferenceId = ?;
48 |
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/Room.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE roomTable(
2 | id INTEGER NOT NULL,
3 | conferenceId INTEGER NOT NULL,
4 | name TEXT NOT NULL,
5 | PRIMARY KEY (id, conferenceId),
6 | FOREIGN KEY (conferenceId) REFERENCES conferenceTable(id)
7 | );
8 |
9 | upsert:
10 | INSERT OR REPLACE INTO roomTable(id, conferenceId, name) VALUES(?,?,?);
11 |
12 | selectAll:
13 | SELECT * FROM roomTable WHERE conferenceId = ?;
14 |
15 | selectById:
16 | SELECT * FROM roomTable WHERE id = ? AND conferenceId = ?;
17 |
18 | deleteById {
19 | UPDATE sessionTable SET roomId = NULL WHERE roomId = :roomId AND conferenceId = :conferenceId;
20 | DELETE FROM roomTable WHERE id = :roomId AND conferenceId = :conferenceId;
21 | }
22 |
23 | existsById:
24 | SELECT count(1) FROM roomTable WHERE id = ? AND conferenceId = ?;
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/SessionSpeaker.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE sessionSpeakerTable(
2 | sessionId TEXT NOT NULL,
3 | speakerId TEXT NOT NULL,
4 | conferenceId INTEGER NOT NULL,
5 | displayOrder INTEGER NOT NULL DEFAULT 0,
6 | PRIMARY KEY (sessionId, speakerId, conferenceId),
7 | FOREIGN KEY (sessionId, conferenceId) REFERENCES sessionTable(id, conferenceId),
8 | FOREIGN KEY (speakerId, conferenceId) REFERENCES profileTable(id, conferenceId),
9 | FOREIGN KEY (conferenceId) REFERENCES conferenceTable(id)
10 | );
11 |
12 | insertUpdate:
13 | INSERT OR REPLACE INTO sessionSpeakerTable(sessionId, speakerId, conferenceId, displayOrder)
14 | VALUES (?,?,?,?);
15 |
16 | selectBySessionId:
17 | SELECT * FROM sessionSpeakerTable WHERE sessionId = ? AND conferenceId = ?;
18 |
19 | deleteBySessionId:
20 | DELETE FROM sessionSpeakerTable WHERE sessionId = ? AND conferenceId = ?;
21 |
22 | selectBySpeakerId:
23 | SELECT * FROM sessionSpeakerTable WHERE speakerId = ? AND conferenceId = ?;
24 |
25 | deleteBySpeakerId:
26 | DELETE FROM sessionSpeakerTable WHERE speakerId = ? AND conferenceId = ?;
27 |
28 | deleteAll:
29 | DELETE FROM sessionSpeakerTable WHERE conferenceId = ?;
30 |
31 |
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/Sponsor.sq:
--------------------------------------------------------------------------------
1 | import kotlin.Boolean;
2 |
3 | CREATE TABLE sponsorTable(
4 | name TEXT NOT NULL,
5 | groupName TEXT NOT NULL,
6 | conferenceId INTEGER NOT NULL,
7 | hasDetail INTEGER AS Boolean NOT NULL,
8 | description TEXT,
9 | iconUrl TEXT NOT NULL,
10 | url TEXT NOT NULL,
11 | PRIMARY KEY (name, groupName, conferenceId),
12 | FOREIGN KEY (groupName, conferenceId) REFERENCES sponsorGroupTable(name, conferenceId),
13 | FOREIGN KEY (conferenceId) REFERENCES conferenceTable(id)
14 | );
15 |
16 | upsert:
17 | INSERT OR REPLACE INTO sponsorTable(name, groupName, conferenceId, hasDetail, description, iconUrl, url)
18 | VALUES (?, ?, ?, ?, ?, ?, ?);
19 |
20 | selectAll:
21 | SELECT * FROM sponsorTable WHERE conferenceId = ?;
22 |
23 | sponsorsByGroup:
24 | SELECT * FROM sponsorTable WHERE groupName = ? AND conferenceId = ?;
25 |
26 | sponsorById:
27 | SELECT * FROM sponsorTable WHERE name = ? AND groupName = ? AND conferenceId = ? LIMIT 1;
28 |
29 | existsById:
30 | SELECT count(1) FROM sponsorTable WHERE name = ? AND groupName = ? AND conferenceId = ? LIMIT 1;
31 |
32 | deleteById:
33 | DELETE FROM sponsorTable WHERE name = ? AND groupName = ? AND conferenceId = ?;
34 |
35 | deleteAll:
36 | DELETE FROM sponsorTable WHERE conferenceId = ?;
37 |
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/SponsorGroup.sq:
--------------------------------------------------------------------------------
1 | import kotlin.Boolean;
2 | import kotlin.Int;
3 |
4 | CREATE TABLE sponsorGroupTable(
5 | name TEXT NOT NULL,
6 | conferenceId INTEGER NOT NULL,
7 | displayPriority INTEGER AS Int NOT NULL,
8 | prominent INTEGER AS Boolean NOT NULL,
9 | PRIMARY KEY (name, conferenceId),
10 | FOREIGN KEY (conferenceId) REFERENCES conferenceTable(id)
11 | );
12 |
13 | upsert:
14 | INSERT OR REPLACE INTO sponsorGroupTable(name, conferenceId, displayPriority, prominent)
15 | VALUES (?, ?, ?, ?);
16 |
17 | selectAll:
18 | SELECT * FROM sponsorGroupTable WHERE conferenceId = ?;
19 |
20 | sponsorGroupByName:
21 | SELECT * FROM sponsorGroupTable WHERE name = ? AND conferenceId = ? LIMIT 1;
22 |
23 | existsByName:
24 | SELECT count(1) FROM sponsorGroupTable WHERE name = ? AND conferenceId = ? LIMIT 1;
25 |
26 | deleteAll:
27 | DELETE FROM sponsorGroupTable WHERE conferenceId = ?;
28 |
29 | deleteByName:
30 | DELETE FROM sponsorGroupTable WHERE name = ? AND conferenceId = ?;
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/SponsorRepresentative.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE sponsorRepresentativeTable(
2 | sponsorName TEXT NOT NULL,
3 | sponsorGroupName TEXT NOT NULL,
4 | representativeId TEXT NOT NULL,
5 | conferenceId INTEGER NOT NULL,
6 | displayOrder INTEGER NOT NULL DEFAULT 0,
7 | PRIMARY KEY (sponsorName, sponsorGroupName, representativeId, conferenceId),
8 | FOREIGN KEY (sponsorName, sponsorGroupName, conferenceId) REFERENCES sponsorTable(name, groupName, conferenceId),
9 | FOREIGN KEY (representativeId, conferenceId) REFERENCES profileTable(id, conferenceId),
10 | FOREIGN KEY (conferenceId) REFERENCES conferenceTable(id)
11 | );
12 |
13 | insertUpdate:
14 | INSERT OR REPLACE INTO sponsorRepresentativeTable(sponsorName, sponsorGroupName, representativeId, conferenceId, displayOrder)
15 | VALUES (?,?,?,?,?);
16 |
17 | selectBySponsorId:
18 | SELECT * FROM sponsorRepresentativeTable WHERE sponsorName = ? AND sponsorGroupName = ? AND conferenceId = ?;
19 |
20 | deleteBySponsorId:
21 | DELETE FROM sponsorRepresentativeTable WHERE sponsorName = ? AND sponsorGroupName = ? AND conferenceId = ?;
22 |
23 | deleteAll:
24 | DELETE FROM sponsorRepresentativeTable WHERE conferenceId = ?;
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/co/touchlab/droidcon/MainScope.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon
2 |
3 | import co.touchlab.droidcon.util.printThrowable
4 | import co.touchlab.kermit.Logger
5 | import kotlin.coroutines.CoroutineContext
6 | import kotlinx.coroutines.CoroutineExceptionHandler
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.SupervisorJob
9 |
10 | class MainScope(private val mainContext: CoroutineContext, private val log: Logger) : CoroutineScope {
11 |
12 | override val coroutineContext: CoroutineContext
13 | get() = mainContext + job + exceptionHandler
14 |
15 | internal val job = SupervisorJob()
16 | private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
17 | printThrowable(throwable)
18 | showError(throwable)
19 | }
20 |
21 | // TODO: Some way of exposing this to the caller without trapping a reference and freezing it.
22 | private fun showError(t: Throwable) {
23 | log.e(throwable = t) { "Error in MainScope" }
24 | }
25 |
26 | fun onDestroy() {
27 | job.cancel()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.domain.repository.impl
2 |
3 | import app.cash.sqldelight.db.SqlDriver
4 | import app.cash.sqldelight.driver.native.NativeSqliteDriver
5 | import co.touchlab.droidcon.db.DroidconDatabase
6 |
7 | actual class SqlDelightDriverFactory {
8 | actual fun createDriver(): SqlDriver = NativeSqliteDriver(DroidconDatabase.Schema, "droidcon.db")
9 | }
10 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/AppInit.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | import co.touchlab.crashkios.crashlytics.enableCrashlytics
4 | import co.touchlab.crashkios.crashlytics.setCrashlyticsUnhandledExceptionHook
5 | import co.touchlab.kermit.ExperimentalKermitApi
6 | import co.touchlab.kermit.Logger
7 | import co.touchlab.kermit.Severity
8 | import co.touchlab.kermit.crashlytics.CrashlyticsLogWriter
9 |
10 | @OptIn(ExperimentalKermitApi::class)
11 | fun setupKermit() {
12 | Logger.addLogWriter(CrashlyticsLogWriter(minSeverity = Severity.Info, minCrashSeverity = Severity.Warn))
13 | enableCrashlytics()
14 | setCrashlyticsUnhandledExceptionHook()
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/BundleResourceReader.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | import co.touchlab.droidcon.domain.service.impl.ResourceReader
4 | import kotlinx.cinterop.BetaInteropApi
5 | import kotlinx.cinterop.ObjCObjectVar
6 | import kotlinx.cinterop.alloc
7 | import kotlinx.cinterop.memScoped
8 | import kotlinx.cinterop.ptr
9 | import kotlinx.cinterop.value
10 | import platform.Foundation.NSBundle
11 | import platform.Foundation.NSError
12 | import platform.Foundation.NSString
13 | import platform.Foundation.NSUTF8StringEncoding
14 | import platform.Foundation.stringWithContentsOfFile
15 | import platform.darwin.NSObject
16 | import platform.darwin.NSObjectMeta
17 |
18 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
19 | @BetaInteropApi
20 | class BundleResourceReader(private val bundle: NSBundle = NSBundle.bundleForClass(BundleMarker)) : ResourceReader {
21 | override fun readResource(name: String): String {
22 | // TODO: Catch iOS-only exceptions and map them to common ones.
23 | val (filename, type) = when (val lastPeriodIndex = name.lastIndexOf('.')) {
24 | 0 -> {
25 | null to name.drop(1)
26 | }
27 |
28 | in 1..Int.MAX_VALUE -> {
29 | name.take(lastPeriodIndex) to name.drop(lastPeriodIndex + 1)
30 | }
31 |
32 | else -> {
33 | name to null
34 | }
35 | }
36 | val path = bundle.pathForResource(filename, type) ?: error(
37 | "Couldn't get path of $name (parsed as: ${listOfNotNull(filename, type).joinToString(".")})",
38 | )
39 |
40 | return memScoped {
41 | val errorPtr = alloc>()
42 |
43 | NSString.stringWithContentsOfFile(path, encoding = NSUTF8StringEncoding, error = errorPtr.ptr) ?: run {
44 | // TODO: Check the NSError and throw common exception.
45 | error("Couldn't load resource: $name. Error: ${errorPtr.value?.localizedDescription} - ${errorPtr.value}")
46 | }
47 | }
48 | }
49 |
50 | private class BundleMarker : NSObject() {
51 | companion object : NSObjectMeta()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/Platform.ios.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | internal actual fun printThrowable(t: Throwable) {
4 | t.printStackTrace()
5 | }
6 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/WrapMultiThreadCallback.kt:
--------------------------------------------------------------------------------
1 | package co.touchlab.droidcon.util
2 |
3 | import kotlinx.coroutines.CompletableDeferred
4 |
5 | // Closures crash because of multi-thread execution, these methods prevent that.
6 | suspend fun wrapMultiThreadCallback(call: (callback: (T) -> Unit) -> Unit): T {
7 | val completable = CompletableDeferred()
8 | val closure: (T) -> Unit = { completable.complete(it) }
9 | call(closure)
10 | return completable.await()
11 | }
12 |
13 | suspend fun wrapMultiThreadCallback(call: (callback: (T1, T2) -> Unit) -> Unit): Pair {
14 | val completable = CompletableDeferred>()
15 | val closure: (T1, T2) -> Unit = { t1, t2 -> completable.complete(t1 to t2) }
16 | call(closure)
17 | return completable.await()
18 | }
19 |
--------------------------------------------------------------------------------
/tlsmall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/tlsmall.png
--------------------------------------------------------------------------------
/venuemaps/dcldn-2024-venue-map-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/venuemaps/dcldn-2024-venue-map-1.jpg
--------------------------------------------------------------------------------
/venuemaps/dcnyc-2024-venue-map-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/touchlab/DroidconKotlin/537585897db8c4f24df70efd89254858cee5f121/venuemaps/dcnyc-2024-venue-map-1.jpg
--------------------------------------------------------------------------------