Merge pull request #1 from lihenggui/compose_multiplatform
WIP: Compose multiplatform supportpull/1323/head
commit
242878e50e
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.google.samples.apps.nowinandroid.configureFlavors
|
||||
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
|
||||
import com.google.samples.apps.nowinandroid.configureKotlinAndroid
|
||||
import com.google.samples.apps.nowinandroid.configureKotlinMultiplatform
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
class KmpLibraryConventionPlugin: Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
plugins.apply("com.android.library")
|
||||
plugins.apply("org.jetbrains.kotlin.multiplatform")
|
||||
configureKotlinMultiplatform()
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.targetSdk = 34
|
||||
configureFlavors(this)
|
||||
configureGradleManagedDevices(this)
|
||||
// The resource prefix is derived from the module name,
|
||||
// so resources inside ":core:module1" must be prefixed with "core_module1_"
|
||||
resourcePrefix = path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_").lowercase() + "_"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.google.samples.apps.nowinandroid.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class KotlinInjectConventionPlugin: Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.google.devtools.ksp")
|
||||
}
|
||||
dependencies {
|
||||
add("kspCommonMainMetadata", libs.findLibrary("kotlin.inject.compiler.ksp").get())
|
||||
add("commonMainImplementation", libs.findLibrary("kotlin.inject.runtime").get())
|
||||
// KSP will eventually have better multiplatform support and we'll be able to simply have
|
||||
// `ksp libs.kotlinInject.compiler` in the dependencies block of each source set
|
||||
// https://github.com/google/ksp/pull/1021
|
||||
add("kspIosX64", libs.findLibrary("kotlin.inject.compiler.ksp").get())
|
||||
add("kspIosArm64", libs.findLibrary("kotlin.inject.compiler.ksp").get())
|
||||
add("kspIosSimulatorArm64", libs.findLibrary("kotlin.inject.compiler.ksp").get())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.google.samples.apps.nowinandroid.configureFlavors
|
||||
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
|
||||
import com.google.samples.apps.nowinandroid.configureKotlinAndroid
|
||||
import com.google.samples.apps.nowinandroid.configurePrintApksTask
|
||||
import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.gradle.kotlin.dsl.kotlin
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||
|
||||
class SqlDelightConventionPlugin: Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
pluginManager.apply("app.cash.sqldelight")
|
||||
extensions.configure<KotlinMultiplatformExtension> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid
|
||||
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jetbrains.kotlin.konan.target.HostManager
|
||||
|
||||
/**
|
||||
* A plugin that applies the Kotlin Multiplatform plugin and configures it for the project.
|
||||
* https://github.com/cashapp/sqldelight/blob/master/buildLogic/multiplatform-convention/src/main/kotlin/app/cash/sqldelight/multiplatform/MultiplatformConventions.kt
|
||||
*/
|
||||
internal fun Project.configureKotlinMultiplatform() {
|
||||
extensions.configure<KotlinMultiplatformExtension> {
|
||||
// Enable native group by default
|
||||
// https://kotlinlang.org/docs/whatsnew1820.html#new-approach-to-source-set-hierarchy
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
jvm()
|
||||
androidTarget()
|
||||
|
||||
js {
|
||||
browser {
|
||||
testTask {
|
||||
useKarma {
|
||||
useChromeHeadless()
|
||||
}
|
||||
}
|
||||
}
|
||||
compilations.configureEach {
|
||||
kotlinOptions {
|
||||
moduleKind = "umd"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tier 1
|
||||
linuxX64()
|
||||
macosX64()
|
||||
macosArm64()
|
||||
iosSimulatorArm64()
|
||||
iosX64()
|
||||
|
||||
// Fix :core:database:linuxArm64Main: Could not resolve me.tatarka.inject:kotlin-inject-runtime:0.6.3.
|
||||
// // tier 2
|
||||
// linuxArm64()
|
||||
// watchosSimulatorArm64()
|
||||
// watchosX64()
|
||||
// watchosArm32()
|
||||
// watchosArm64()
|
||||
// tvosSimulatorArm64()
|
||||
// tvosX64()
|
||||
// tvosArm64()
|
||||
iosArm64()
|
||||
|
||||
// fix :core:model:androidNativeArm32Main: Could not resolve org.jetbrains.kotlinx:kotlinx-datetime
|
||||
// // tier 3
|
||||
// androidNativeArm32()
|
||||
// androidNativeArm64()
|
||||
// androidNativeX86()
|
||||
// androidNativeX64()
|
||||
// mingwX64()
|
||||
// watchosDeviceArm64()
|
||||
|
||||
// linking fails for the linux test build if not built on a linux host
|
||||
// ensure the tests and linking for them is only done on linux hosts
|
||||
project.tasks.named("linuxX64Test") { enabled = HostManager.hostIsLinux }
|
||||
project.tasks.named("linkDebugTestLinuxX64") { enabled = HostManager.hostIsLinux }
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf(
|
||||
// Suppress warning:'expect'/'actual' classes (including interfaces, objects,
|
||||
// annotations, enums, and 'actual' typealiases) are in Beta.
|
||||
"-Xexpect-actual-classes",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Fixes Cannot locate tasks that match ':core:model:testClasses' as task 'testClasses'
|
||||
// not found in project ':core:model'. Some candidates are: 'jsTestClasses', 'jvmTestClasses'.
|
||||
project.tasks.create("testClasses") {
|
||||
dependsOn("allTests")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import android.content.Context
|
||||
import app.cash.sqldelight.async.coroutines.synchronous
|
||||
import app.cash.sqldelight.db.QueryResult
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.db.SqlSchema
|
||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import me.tatarka.inject.annotations.Provides
|
||||
|
||||
@Inject
|
||||
actual class DriverModule(private val context: Context) {
|
||||
|
||||
@Provides
|
||||
actual suspend fun provideDbDriver(
|
||||
schema: SqlSchema<QueryResult.AsyncValue<Unit>>,
|
||||
): SqlDriver {
|
||||
return AndroidSqliteDriver(schema.synchronous(), context, "nia-database.db")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
||||
|
||||
actual suspend fun createDriver(): SqlDriver {
|
||||
return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
.also { NiaDatabase.Schema.create(it).await() }
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
/**
|
||||
* DAO for [NewsResource] and [NewsResourceEntity] access
|
||||
*/
|
||||
class NewsResourceDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) {
|
||||
private val query = db.newsResourceQueries
|
||||
|
||||
/**
|
||||
* Fetches news resources that match the query parameters
|
||||
*/
|
||||
fun getNewsResources(
|
||||
useFilterTopicIds: Boolean = false,
|
||||
filterTopicIds: Set<String> = emptySet(),
|
||||
useFilterNewsIds: Boolean = false,
|
||||
filterNewsIds: Set<String> = emptySet(),
|
||||
): Flow<List<PopulatedNewsResource>> {
|
||||
return query.getNewsResources(
|
||||
useFilterTopicIds = useFilterTopicIds,
|
||||
filterTopicIds = filterTopicIds,
|
||||
useFilterNewsIds = useFilterNewsIds,
|
||||
filterNewsIds = filterNewsIds,
|
||||
) { id, title, content, url, headerImageUrl, publishDate, type ->
|
||||
PopulatedNewsResource(
|
||||
entity = NewsResourceEntity(
|
||||
id = id,
|
||||
title = title,
|
||||
content = content,
|
||||
url = url,
|
||||
headerImageUrl = headerImageUrl,
|
||||
publishDate = Instant.fromEpochMilliseconds(publishDate),
|
||||
type = type,
|
||||
),
|
||||
// TODO Dealing with NewsResources <-> Topics relationship
|
||||
topics = emptyList(),
|
||||
)
|
||||
}
|
||||
.asFlow()
|
||||
.mapToList(dispatcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches ids of news resources that match the query parameters
|
||||
*/
|
||||
fun getNewsResourceIds(
|
||||
useFilterTopicIds: Boolean = false,
|
||||
filterTopicIds: Set<String> = emptySet(),
|
||||
useFilterNewsIds: Boolean = false,
|
||||
filterNewsIds: Set<String> = emptySet(),
|
||||
): Flow<List<String>> {
|
||||
return query.getNewsResourceIds(
|
||||
useFilterTopicIds = useFilterTopicIds,
|
||||
filterTopicIds = filterTopicIds,
|
||||
useFilterNewsIds = useFilterNewsIds,
|
||||
filterNewsIds = filterNewsIds,
|
||||
)
|
||||
.asFlow()
|
||||
.mapToList(dispatcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts [entities] into the db if they don't exist, and ignores those that do
|
||||
*/
|
||||
suspend fun insertOrIgnoreNewsResources(entities: List<NewsResourceEntity>): List<Long> {
|
||||
entities.forEach {
|
||||
query.insertOrIgnoreNewsResource(
|
||||
id = it.id,
|
||||
title = it.title,
|
||||
content = it.content,
|
||||
url = it.url,
|
||||
header_image_url = it.headerImageUrl,
|
||||
publish_date = it.publishDate.toEpochMilliseconds(),
|
||||
type = it.type,
|
||||
)
|
||||
}
|
||||
// TODO Return the inserted ids
|
||||
return entities.mapNotNull {
|
||||
it.id.toLongOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates [newsResourceEntities] in the db under the specified primary keys
|
||||
*/
|
||||
suspend fun upsertNewsResources(newsResourceEntities: List<NewsResourceEntity>) {
|
||||
newsResourceEntities.forEach {
|
||||
query.upsertNewsResource(
|
||||
id = it.id,
|
||||
title = it.title,
|
||||
content = it.content,
|
||||
url = it.url,
|
||||
header_image_url = it.headerImageUrl,
|
||||
publish_date = it.publishDate.toEpochMilliseconds(),
|
||||
type = it.type,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun insertOrIgnoreTopicCrossRefEntities(
|
||||
newsResourceTopicCrossReferences: List<NewsResourceTopicCrossRef>,
|
||||
) {
|
||||
// TODO Consider removing cross references
|
||||
// query.insertOrIgnoreNewsResourceTopicCrossRefs(newsResourceTopicCrossReferences)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes rows in the db matching the specified [ids]
|
||||
*/
|
||||
suspend fun deleteNewsResources(ids: List<String>) {
|
||||
query.deleteNewsResources(ids)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
|
||||
import com.google.samples.apps.nowinandroid.core.database.Recent_search_query
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQueryEntity
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
/**
|
||||
* DAO for [RecentSearchQueryEntity] access
|
||||
*/
|
||||
class RecentSearchQueryDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) {
|
||||
|
||||
private val query = db.recentSearchQueryQueries
|
||||
|
||||
fun getRecentSearchQueryEntities(limit: Int): Flow<List<RecentSearchQueryEntity>> {
|
||||
return query.getRecentSearchQueryEntities(limit.toLong()) { query, timestamp ->
|
||||
RecentSearchQueryEntity(
|
||||
query = query,
|
||||
queriedDate = Instant.fromEpochMilliseconds(timestamp ?: 0L),
|
||||
)
|
||||
}
|
||||
.asFlow()
|
||||
.mapToList(dispatcher)
|
||||
}
|
||||
|
||||
suspend fun insertOrReplaceRecentSearchQuery(recentSearchQuery: RecentSearchQueryEntity) {
|
||||
query.insertOrReplaceRecentSearchQuery(
|
||||
recent_search_query = Recent_search_query(
|
||||
query = recentSearchQuery.query,
|
||||
queried_date = recentSearchQuery.queriedDate.toEpochMilliseconds(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun clearRecentSearchQueries() {
|
||||
query.clearRecentSearchQueries()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import app.cash.sqldelight.coroutines.mapToOne
|
||||
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* DAO for [TopicEntity] access
|
||||
*/
|
||||
|
||||
class TopicDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) {
|
||||
|
||||
private val query = db.topicsQueries
|
||||
|
||||
fun getTopicEntity(topicId: String): Flow<TopicEntity> {
|
||||
return query.getTopicEntity(topicId) { id, name, shortDescription, longDescription, url, imageUrl ->
|
||||
TopicEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
shortDescription = shortDescription,
|
||||
longDescription = longDescription,
|
||||
url = url,
|
||||
imageUrl = imageUrl,
|
||||
)
|
||||
}
|
||||
.asFlow()
|
||||
.mapToOne(dispatcher)
|
||||
}
|
||||
|
||||
fun getTopicEntities(): Flow<List<TopicEntity>> {
|
||||
return query.getOneOffTopicEntities { id, name, shortDescription, longDescription, url, imageUrl ->
|
||||
TopicEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
shortDescription = shortDescription,
|
||||
longDescription = longDescription,
|
||||
url = url,
|
||||
imageUrl = imageUrl,
|
||||
)
|
||||
}
|
||||
.asFlow()
|
||||
.mapToList(dispatcher)
|
||||
}
|
||||
|
||||
suspend fun getOneOffTopicEntities(): List<TopicEntity> {
|
||||
// TODO: Use flow?
|
||||
return query.getOneOffTopicEntities { id, name, shortDescription, longDescription, url, imageUrl ->
|
||||
TopicEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
shortDescription = shortDescription,
|
||||
longDescription = longDescription,
|
||||
url = url,
|
||||
imageUrl = imageUrl,
|
||||
)
|
||||
}.executeAsList()
|
||||
}
|
||||
|
||||
fun getTopicEntities(ids: Set<String>): Flow<List<TopicEntity>> {
|
||||
return query.getTopicEntities { id, name, shortDescription, longDescription, url, imageUrl ->
|
||||
TopicEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
shortDescription = shortDescription,
|
||||
longDescription = longDescription,
|
||||
url = url,
|
||||
imageUrl = imageUrl,
|
||||
)
|
||||
}
|
||||
.asFlow()
|
||||
.mapToList(dispatcher)
|
||||
.map {
|
||||
it.filter { topic -> topic.id in ids }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts [topicEntities] into the db if they don't exist, and ignores those that do
|
||||
*/
|
||||
suspend fun insertOrIgnoreTopics(topicEntities: List<TopicEntity>): List<Long> {
|
||||
topicEntities.map {
|
||||
query.insertOrIgnoreTopic(
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
short_description = it.shortDescription,
|
||||
long_description = it.longDescription,
|
||||
url = it.url,
|
||||
image_url = it.imageUrl,
|
||||
)
|
||||
}
|
||||
// TODO return the ids of the inserted topics
|
||||
return topicEntities.mapNotNull { it.id.toLongOrNull() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates [entities] in the db under the specified primary keys
|
||||
*/
|
||||
suspend fun upsertTopics(entities: List<TopicEntity>) {
|
||||
entities.forEach {
|
||||
query.upsertTopic(
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
short_description = it.shortDescription,
|
||||
long_description = it.longDescription,
|
||||
url = it.url,
|
||||
image_url = it.imageUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes rows in the db matching the specified [ids]
|
||||
*/
|
||||
suspend fun deleteTopics(ids: List<String>) {
|
||||
query.deleteTopics(ids)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import app.cash.sqldelight.coroutines.mapToOne
|
||||
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* DAO for [TopicFtsEntity] access.
|
||||
*/
|
||||
class TopicFtsDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) {
|
||||
|
||||
private val dbQuery = db.topicFtsQueries
|
||||
|
||||
suspend fun insertAll(topics: List<TopicFtsEntity>) {
|
||||
topics.forEach {
|
||||
dbQuery.insert(
|
||||
topic_id = it.topicId,
|
||||
name = it.name,
|
||||
short_description = it.shortDescription,
|
||||
long_description = it.longDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun searchAllTopics(query: String): Flow<List<String>> {
|
||||
return dbQuery.searchAllTopics(query) {
|
||||
it.orEmpty()
|
||||
}
|
||||
.asFlow()
|
||||
.mapToList(dispatcher)
|
||||
}
|
||||
|
||||
fun getCount(): Flow<Int> {
|
||||
return dbQuery.getCount()
|
||||
.asFlow()
|
||||
.mapToOne(dispatcher)
|
||||
.map { it.toInt() }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
CREATE TABLE news_resource (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
header_image_url TEXT,
|
||||
publish_date INTEGER NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
);
|
||||
|
||||
getNewsResources:
|
||||
SELECT * FROM news_resource
|
||||
WHERE
|
||||
CASE WHEN :useFilterNewsIds
|
||||
THEN id IN :filterNewsIds
|
||||
ELSE 1
|
||||
END
|
||||
AND
|
||||
CASE WHEN :useFilterTopicIds
|
||||
THEN id IN
|
||||
(
|
||||
SELECT news_resource_id FROM news_resources_topics
|
||||
WHERE topic_id IN :filterTopicIds
|
||||
)
|
||||
ELSE 1
|
||||
END
|
||||
ORDER BY publish_date DESC;
|
||||
|
||||
getNewsResourceIds:
|
||||
SELECT id FROM news_resource
|
||||
WHERE
|
||||
CASE WHEN :useFilterNewsIds
|
||||
THEN id IN :filterNewsIds
|
||||
ELSE 1
|
||||
END
|
||||
AND
|
||||
CASE WHEN :useFilterTopicIds
|
||||
THEN id IN
|
||||
(
|
||||
SELECT news_resource_id FROM news_resources_topics
|
||||
WHERE topic_id IN :filterTopicIds
|
||||
)
|
||||
ELSE 1
|
||||
END
|
||||
ORDER BY publish_date DESC;
|
||||
|
||||
insertOrIgnoreNewsResource:
|
||||
INSERT OR IGNORE INTO news_resource (id, title, content, url, header_image_url, publish_date, type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
|
||||
upsertNewsResource:
|
||||
INSERT INTO news_resource (id, title, content, url, header_image_url, publish_date, type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
content = excluded.content,
|
||||
url = excluded.url,
|
||||
header_image_url = excluded.header_image_url,
|
||||
publish_date = excluded.publish_date,
|
||||
type = excluded.type;
|
||||
|
||||
insertOrIgnoreTopicCrossRefEntities:
|
||||
INSERT OR IGNORE INTO news_resources_topics (news_resource_id, topic_id)
|
||||
VALUES (?, ?);
|
||||
|
||||
deleteNewsResources:
|
||||
DELETE FROM news_resource
|
||||
WHERE id IN :ids;
|
||||
@ -0,0 +1,26 @@
|
||||
CREATE VIRTUAL TABLE news_resource_fts
|
||||
USING FTS4(
|
||||
news_resource_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
);
|
||||
-- Triggers to keep the FTS index up to date.
|
||||
CREATE TRIGGER news_resource_ai AFTER INSERT ON news_resource BEGIN
|
||||
INSERT INTO news_resource_fts (rowid, news_resource_id, title, content) VALUES (new.rowid, new.id, new.title, new.content);
|
||||
END;
|
||||
CREATE TRIGGER news_resource_ad AFTER DELETE ON news_resource BEGIN
|
||||
INSERT INTO news_resource_fts (news_resource_fts, rowid, news_resource_id, title, content) VALUES ('delete', old.rowid, old.id, old.title, old.content);
|
||||
END;
|
||||
CREATE TRIGGER news_resource_au AFTER UPDATE ON news_resource BEGIN
|
||||
INSERT INTO news_resource_fts (news_resource_fts, rowid, news_resource_id, title, content) VALUES ('delete', old.rowid, old.id, old.title, old.content);
|
||||
INSERT INTO news_resource_fts (rowid, news_resource_id, title, content) VALUES (new.rowid, new.id, new.title, new.content);
|
||||
END;
|
||||
|
||||
insert:
|
||||
INSERT INTO news_resource_fts (news_resource_id, title, content) VALUES (:news_resource_id, :title, :content);
|
||||
|
||||
searchAllNewsResources:
|
||||
SELECT news_resource_id FROM news_resource_fts WHERE news_resource_fts.title MATCH :query;
|
||||
|
||||
getCount:
|
||||
SELECT count(*) FROM news_resource_fts;
|
||||
@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS news_resource_topic (
|
||||
news_resource_id TEXT NOT NULL,
|
||||
topic_id TEXT NOT NULL,
|
||||
PRIMARY KEY(news_resource_id, topic_id),
|
||||
FOREIGN KEY(news_resource_id) REFERENCES news_resource(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(topic_id) REFERENCES topic(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_news_resource_topic_news_resource_id ON news_resource_topic(news_resource_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_news_resource_topic_topic_id ON news_resource_topic(topic_id);
|
||||
@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS news_resources_topics (
|
||||
news_resource_id TEXT NOT NULL,
|
||||
topic_id TEXT NOT NULL,
|
||||
PRIMARY KEY (news_resource_id, topic_id),
|
||||
FOREIGN KEY (news_resource_id) REFERENCES news_resource(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (topic_id) REFERENCES topic(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_news_resource_id ON news_resource_topic(news_resource_id);
|
||||
|
||||
CREATE INDEX idx_topic_id ON news_resource_topic(topic_id);
|
||||
@ -0,0 +1,13 @@
|
||||
CREATE TABLE recent_search_query (
|
||||
query TEXT NOT NULL PRIMARY KEY,
|
||||
queried_date INTEGER DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
getRecentSearchQueryEntities:
|
||||
SELECT * FROM recent_search_query ORDER BY queried_date DESC LIMIT :limit;
|
||||
|
||||
insertOrReplaceRecentSearchQuery:
|
||||
INSERT OR REPLACE INTO recent_search_query (query) VALUES :query;
|
||||
|
||||
clearRecentSearchQueries:
|
||||
DELETE FROM recent_search_query;
|
||||
@ -0,0 +1,41 @@
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS topic_fts
|
||||
USING FTS4(
|
||||
topic_id TEXT,
|
||||
name TEXT,
|
||||
short_description TEXT,
|
||||
long_description TEXT
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS topic_ai AFTER INSERT ON topic
|
||||
BEGIN
|
||||
INSERT INTO topic_fts (topic_id, name, short_description, long_description)
|
||||
VALUES (new.id, new.name, new.short_description, new.long_description);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS topic_ad AFTER DELETE ON topic
|
||||
BEGIN
|
||||
DELETE FROM topic_fts WHERE topic_id = old.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS topic_au AFTER UPDATE ON topic
|
||||
BEGIN
|
||||
UPDATE topic_fts SET
|
||||
name = new.name,
|
||||
short_description = new.short_description,
|
||||
long_description = new.long_description
|
||||
WHERE topic_id = new.id;
|
||||
END;
|
||||
|
||||
insert:
|
||||
INSERT INTO topic_fts (topic_id, name, short_description, long_description)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(topic_id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
short_description = excluded.short_description,
|
||||
long_description = excluded.long_description;
|
||||
|
||||
searchAllTopics:
|
||||
SELECT topic_id FROM topic_fts WHERE topic_fts MATCH :query;
|
||||
|
||||
getCount:
|
||||
SELECT count(*) FROM topic_fts;
|
||||
@ -0,0 +1,34 @@
|
||||
CREATE TABLE topic (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
short_description TEXT NOT NULL,
|
||||
long_description TEXT NOT NULL DEFAULT '',
|
||||
url TEXT NOT NULL DEFAULT '',
|
||||
image_url TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
getTopicEntity:
|
||||
SELECT * FROM topic WHERE id = :topicId;
|
||||
|
||||
getTopicEntities:
|
||||
SELECT * FROM topic;
|
||||
|
||||
getOneOffTopicEntities:
|
||||
SELECT * FROM topic;
|
||||
|
||||
insertOrIgnoreTopic:
|
||||
INSERT OR IGNORE INTO topic(id, name, short_description, long_description, url, image_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?);
|
||||
|
||||
upsertTopic:
|
||||
INSERT INTO topic(id, name, short_description, long_description, url, image_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
short_description = excluded.short_description,
|
||||
long_description = excluded.long_description,
|
||||
url = excluded.url,
|
||||
image_url = excluded.image_url;
|
||||
|
||||
deleteTopics:
|
||||
DELETE FROM topic WHERE id IN :ids;
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
||||
/**
|
||||
* Init driver for each platform. Should *always* be called to setup test
|
||||
*/
|
||||
expect suspend fun createDriver(): SqlDriver
|
||||
fun testing(block: suspend CoroutineScope.(NiaDatabase) -> Unit) = runTest {
|
||||
val driver = createDriver()
|
||||
block(NiaDatabase(driver))
|
||||
driver.close()
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import app.cash.sqldelight.db.QueryResult
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.db.SqlSchema
|
||||
import app.cash.sqldelight.driver.worker.WebWorkerDriver
|
||||
import me.tatarka.inject.annotations.Provides
|
||||
import org.w3c.dom.Worker
|
||||
|
||||
actual class DriverModule {
|
||||
@Provides
|
||||
actual suspend fun provideDbDriver(
|
||||
schema: SqlSchema<QueryResult.AsyncValue<Unit>>,
|
||||
): SqlDriver {
|
||||
return WebWorkerDriver(
|
||||
Worker(
|
||||
js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)"""),
|
||||
),
|
||||
).also { schema.create(it).await() }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.worker.WebWorkerDriver
|
||||
import org.w3c.dom.Worker
|
||||
|
||||
actual suspend fun createDriver(): SqlDriver {
|
||||
return WebWorkerDriver(
|
||||
Worker(
|
||||
js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)"""),
|
||||
),
|
||||
).also { NiaDatabase.Schema.create(it).await() }
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import app.cash.sqldelight.db.QueryResult
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.db.SqlSchema
|
||||
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
||||
import me.tatarka.inject.annotations.Provides
|
||||
|
||||
actual class DriverModule {
|
||||
@Provides
|
||||
actual suspend fun provideDbDriver(
|
||||
schema: SqlSchema<QueryResult.AsyncValue<Unit>>,
|
||||
): SqlDriver {
|
||||
return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
.also { schema.create(it).await() }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
||||
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase.Companion.Schema
|
||||
|
||||
actual suspend fun createDriver(): SqlDriver {
|
||||
return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
.also { Schema.create(it).await() }
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.RenameColumn
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
|
||||
/**
|
||||
* Automatic schema migrations sometimes require extra instructions to perform the migration, for
|
||||
* example, when a column is renamed. These extra instructions are placed here by creating a class
|
||||
* using the following naming convention `SchemaXtoY` where X is the schema version you're migrating
|
||||
* from and Y is the schema version you're migrating to. The class should implement
|
||||
* `AutoMigrationSpec`.
|
||||
*/
|
||||
internal object DatabaseMigrations {
|
||||
|
||||
@RenameColumn(
|
||||
tableName = "topics",
|
||||
fromColumnName = "description",
|
||||
toColumnName = "shortDescription",
|
||||
)
|
||||
class Schema2to3 : AutoMigrationSpec
|
||||
|
||||
@DeleteColumn(
|
||||
tableName = "news_resources",
|
||||
columnName = "episode_id",
|
||||
)
|
||||
@DeleteTable.Entries(
|
||||
DeleteTable(
|
||||
tableName = "episodes_authors",
|
||||
),
|
||||
DeleteTable(
|
||||
tableName = "episodes",
|
||||
),
|
||||
)
|
||||
class Schema10to11 : AutoMigrationSpec
|
||||
|
||||
@DeleteTable.Entries(
|
||||
DeleteTable(
|
||||
tableName = "news_resources_authors",
|
||||
),
|
||||
DeleteTable(
|
||||
tableName = "authors",
|
||||
),
|
||||
)
|
||||
class Schema11to12 : AutoMigrationSpec
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.RecentSearchQueryDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQueryEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.util.InstantConverter
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
NewsResourceEntity::class,
|
||||
NewsResourceTopicCrossRef::class,
|
||||
NewsResourceFtsEntity::class,
|
||||
TopicEntity::class,
|
||||
TopicFtsEntity::class,
|
||||
RecentSearchQueryEntity::class,
|
||||
],
|
||||
version = 14,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 1, to = 2),
|
||||
AutoMigration(from = 2, to = 3, spec = DatabaseMigrations.Schema2to3::class),
|
||||
AutoMigration(from = 3, to = 4),
|
||||
AutoMigration(from = 4, to = 5),
|
||||
AutoMigration(from = 5, to = 6),
|
||||
AutoMigration(from = 6, to = 7),
|
||||
AutoMigration(from = 7, to = 8),
|
||||
AutoMigration(from = 8, to = 9),
|
||||
AutoMigration(from = 9, to = 10),
|
||||
AutoMigration(from = 10, to = 11, spec = DatabaseMigrations.Schema10to11::class),
|
||||
AutoMigration(from = 11, to = 12, spec = DatabaseMigrations.Schema11to12::class),
|
||||
AutoMigration(from = 12, to = 13),
|
||||
AutoMigration(from = 13, to = 14),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(
|
||||
InstantConverter::class,
|
||||
)
|
||||
internal abstract class NiaDatabase : RoomDatabase() {
|
||||
abstract fun topicDao(): TopicDao
|
||||
abstract fun newsResourceDao(): NewsResourceDao
|
||||
abstract fun topicFtsDao(): TopicFtsDao
|
||||
abstract fun newsResourceFtsDao(): NewsResourceFtsDao
|
||||
abstract fun recentSearchQueryDao(): RecentSearchQueryDao
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* DAO for [NewsResource] and [NewsResourceEntity] access
|
||||
*/
|
||||
@Dao
|
||||
interface NewsResourceDao {
|
||||
|
||||
/**
|
||||
* Fetches news resources that match the query parameters
|
||||
*/
|
||||
@Transaction
|
||||
@Query(
|
||||
value = """
|
||||
SELECT * FROM news_resources
|
||||
WHERE
|
||||
CASE WHEN :useFilterNewsIds
|
||||
THEN id IN (:filterNewsIds)
|
||||
ELSE 1
|
||||
END
|
||||
AND
|
||||
CASE WHEN :useFilterTopicIds
|
||||
THEN id IN
|
||||
(
|
||||
SELECT news_resource_id FROM news_resources_topics
|
||||
WHERE topic_id IN (:filterTopicIds)
|
||||
)
|
||||
ELSE 1
|
||||
END
|
||||
ORDER BY publish_date DESC
|
||||
""",
|
||||
)
|
||||
fun getNewsResources(
|
||||
useFilterTopicIds: Boolean = false,
|
||||
filterTopicIds: Set<String> = emptySet(),
|
||||
useFilterNewsIds: Boolean = false,
|
||||
filterNewsIds: Set<String> = emptySet(),
|
||||
): Flow<List<PopulatedNewsResource>>
|
||||
|
||||
/**
|
||||
* Fetches ids of news resources that match the query parameters
|
||||
*/
|
||||
@Transaction
|
||||
@Query(
|
||||
value = """
|
||||
SELECT id FROM news_resources
|
||||
WHERE
|
||||
CASE WHEN :useFilterNewsIds
|
||||
THEN id IN (:filterNewsIds)
|
||||
ELSE 1
|
||||
END
|
||||
AND
|
||||
CASE WHEN :useFilterTopicIds
|
||||
THEN id IN
|
||||
(
|
||||
SELECT news_resource_id FROM news_resources_topics
|
||||
WHERE topic_id IN (:filterTopicIds)
|
||||
)
|
||||
ELSE 1
|
||||
END
|
||||
ORDER BY publish_date DESC
|
||||
""",
|
||||
)
|
||||
fun getNewsResourceIds(
|
||||
useFilterTopicIds: Boolean = false,
|
||||
filterTopicIds: Set<String> = emptySet(),
|
||||
useFilterNewsIds: Boolean = false,
|
||||
filterNewsIds: Set<String> = emptySet(),
|
||||
): Flow<List<String>>
|
||||
|
||||
/**
|
||||
* Inserts [entities] into the db if they don't exist, and ignores those that do
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertOrIgnoreNewsResources(entities: List<NewsResourceEntity>): List<Long>
|
||||
|
||||
/**
|
||||
* Inserts or updates [newsResourceEntities] in the db under the specified primary keys
|
||||
*/
|
||||
@Upsert
|
||||
suspend fun upsertNewsResources(newsResourceEntities: List<NewsResourceEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertOrIgnoreTopicCrossRefEntities(
|
||||
newsResourceTopicCrossReferences: List<NewsResourceTopicCrossRef>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Deletes rows in the db matching the specified [ids]
|
||||
*/
|
||||
@Query(
|
||||
value = """
|
||||
DELETE FROM news_resources
|
||||
WHERE id in (:ids)
|
||||
""",
|
||||
)
|
||||
suspend fun deleteNewsResources(ids: List<String>)
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQueryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* DAO for [RecentSearchQueryEntity] access
|
||||
*/
|
||||
@Dao
|
||||
interface RecentSearchQueryDao {
|
||||
@Query(value = "SELECT * FROM recentSearchQueries ORDER BY queriedDate DESC LIMIT :limit")
|
||||
fun getRecentSearchQueryEntities(limit: Int): Flow<List<RecentSearchQueryEntity>>
|
||||
|
||||
@Upsert
|
||||
suspend fun insertOrReplaceRecentSearchQuery(recentSearchQuery: RecentSearchQueryEntity)
|
||||
|
||||
@Query(value = "DELETE FROM recentSearchQueries")
|
||||
suspend fun clearRecentSearchQueries()
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* DAO for [TopicEntity] access
|
||||
*/
|
||||
@Dao
|
||||
interface TopicDao {
|
||||
@Query(
|
||||
value = """
|
||||
SELECT * FROM topics
|
||||
WHERE id = :topicId
|
||||
""",
|
||||
)
|
||||
fun getTopicEntity(topicId: String): Flow<TopicEntity>
|
||||
|
||||
@Query(value = "SELECT * FROM topics")
|
||||
fun getTopicEntities(): Flow<List<TopicEntity>>
|
||||
|
||||
@Query(value = "SELECT * FROM topics")
|
||||
suspend fun getOneOffTopicEntities(): List<TopicEntity>
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
SELECT * FROM topics
|
||||
WHERE id IN (:ids)
|
||||
""",
|
||||
)
|
||||
fun getTopicEntities(ids: Set<String>): Flow<List<TopicEntity>>
|
||||
|
||||
/**
|
||||
* Inserts [topicEntities] into the db if they don't exist, and ignores those that do
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertOrIgnoreTopics(topicEntities: List<TopicEntity>): List<Long>
|
||||
|
||||
/**
|
||||
* Inserts or updates [entities] in the db under the specified primary keys
|
||||
*/
|
||||
@Upsert
|
||||
suspend fun upsertTopics(entities: List<TopicEntity>)
|
||||
|
||||
/**
|
||||
* Deletes rows in the db matching the specified [ids]
|
||||
*/
|
||||
@Query(
|
||||
value = """
|
||||
DELETE FROM topics
|
||||
WHERE id in (:ids)
|
||||
""",
|
||||
)
|
||||
suspend fun deleteTopics(ids: List<String>)
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* DAO for [TopicFtsEntity] access.
|
||||
*/
|
||||
@Dao
|
||||
interface TopicFtsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(topics: List<TopicFtsEntity>)
|
||||
|
||||
@Query("SELECT topicId FROM topicsFts WHERE topicsFts MATCH :query")
|
||||
fun searchAllTopics(query: String): Flow<List<String>>
|
||||
|
||||
@Query("SELECT count(*) FROM topicsFts")
|
||||
fun getCount(): Flow<Int>
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import app.cash.sqldelight.async.coroutines.synchronous
|
||||
import app.cash.sqldelight.db.QueryResult
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.db.SqlSchema
|
||||
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
||||
import me.tatarka.inject.annotations.Provides
|
||||
|
||||
actual class DriverModule {
|
||||
|
||||
@Provides
|
||||
actual suspend fun provideDbDriver(
|
||||
schema: SqlSchema<QueryResult.AsyncValue<Unit>>,
|
||||
): SqlDriver {
|
||||
return NativeSqliteDriver(schema.synchronous(), "nia-database.db")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.database
|
||||
|
||||
import app.cash.sqldelight.async.coroutines.synchronous
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
||||
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase.Companion.Schema
|
||||
|
||||
actual suspend fun createDriver(): SqlDriver {
|
||||
return NativeSqliteDriver(Schema.synchronous(), "nia-database-test.db")
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
TEAM_ID=
|
||||
BUNDLE_ID=com.google.samples.apps.nowinandroid
|
||||
APP_NAME=nowinandroid
|
||||
@ -0,0 +1,383 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
|
||||
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
|
||||
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
|
||||
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
|
||||
7555FF7B242A565900829871 /* .app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = .app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
058557D7273AAEEB004C7B11 /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
42799AB246E5F90AF97AA0EF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7555FF72242A565900829871 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AB1DB47929225F7C00F7AF9C /* Configuration */,
|
||||
7555FF7D242A565900829871 /* iosApp */,
|
||||
7555FF7C242A565900829871 /* Products */,
|
||||
42799AB246E5F90AF97AA0EF /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7555FF7C242A565900829871 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7555FF7B242A565900829871 /* .app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7555FF7D242A565900829871 /* iosApp */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
058557BA273AAA24004C7B11 /* Assets.xcassets */,
|
||||
7555FF82242A565900829871 /* ContentView.swift */,
|
||||
7555FF8C242A565B00829871 /* Info.plist */,
|
||||
2152FB032600AC8F00CF470E /* iOSApp.swift */,
|
||||
058557D7273AAEEB004C7B11 /* Preview Content */,
|
||||
);
|
||||
path = iosApp;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AB1DB47929225F7C00F7AF9C /* Configuration */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AB3632DC29227652001CCB65 /* Config.xcconfig */,
|
||||
);
|
||||
path = Configuration;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
7555FF7A242A565900829871 /* iosApp */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
|
||||
buildPhases = (
|
||||
F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,
|
||||
7555FF77242A565900829871 /* Sources */,
|
||||
7555FF79242A565900829871 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = iosApp;
|
||||
productName = iosApp;
|
||||
productReference = 7555FF7B242A565900829871 /* .app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
7555FF73242A565900829871 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1130;
|
||||
LastUpgradeCheck = 1130;
|
||||
ORGANIZATIONNAME = orgName;
|
||||
TargetAttributes = {
|
||||
7555FF7A242A565900829871 = {
|
||||
CreatedOnToolsVersion = 11.3.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 7555FF72242A565900829871;
|
||||
productRefGroup = 7555FF7C242A565900829871 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
7555FF7A242A565900829871 /* iosApp */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
7555FF79242A565900829871 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
|
||||
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Compile Kotlin Framework";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
7555FF77242A565900829871 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
|
||||
7555FF83242A565900829871 /* ContentView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
7555FFA3242A565B00829871 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7555FFA4242A565B00829871 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7555FFA6242A565B00829871 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "${TEAM_ID}";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
|
||||
);
|
||||
INFOPLIST_FILE = iosApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-framework",
|
||||
composeApp,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
|
||||
PRODUCT_NAME = "${APP_NAME}";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7555FFA7242A565B00829871 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "${TEAM_ID}";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
|
||||
);
|
||||
INFOPLIST_FILE = iosApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-framework",
|
||||
composeApp,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
|
||||
PRODUCT_NAME = "${APP_NAME}";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7555FFA3242A565B00829871 /* Debug */,
|
||||
7555FFA4242A565B00829871 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7555FFA6242A565B00829871 /* Debug */,
|
||||
7555FFA7242A565B00829871 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 7555FF73242A565900829871 /* Project object */;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-icon-1024.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 66 KiB |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct ComposeView: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
MainViewControllerKt.MainViewController()
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
ComposeView()
|
||||
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct iOSApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue