sonodrill

Sonodrill, app iOS/Android d’entraînement musical (repères, boucles, vitesse), Kotlin Multiplatform + Compose
git clone https://git.schurger.org/sonodrill.git
Log | Files | Refs

commit ea0bf38748c9ee3cb79d6494ff52015129539483
parent 41e4af5e47028a3544d69dbaea5ebaa276666daa
Author: Jean Schurger <jean@schurger.org>
Date:   Wed,  1 Apr 2026 09:23:57 -0400

Add end-of-track detection, Ko-fi gating, zoom, waveform res, and Fastlane

- Playback stops at end of song instead of continuing
- Ko-fi button only appears after 5+ plays (NSUserDefaults counter)
- Pinch-to-zoom on waveform timeline with auto-scroll
- Waveform resolution at 1 sample per 20ms (1/50th second)
- Time label with background for contrast on played sections
- Fix first bar highlight bug
- Fastlane setup for App Store release via ASC API key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
M.gitignore | 5+++++
McomposeApp/src/androidMain/kotlin/ubi2/sonodrill/model/android_platform.kt | 6+++---
McomposeApp/src/commonMain/kotlin/ubi2/sonodrill/model/platform.kt | 2++
McomposeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/app.kt | 2++
McomposeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/library/library_screen.kt | 7+++++--
McomposeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_screen.kt | 249+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
McomposeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_view_model.kt | 4+++-
McomposeApp/src/iosMain/kotlin/ubi2/sonodrill/audio/ios_audio_player.kt | 9++++++++-
McomposeApp/src/iosMain/kotlin/ubi2/sonodrill/model/ios_platform.kt | 8++++++++
AiosApp/fastlane/Appfile | 3+++
AiosApp/fastlane/Fastfile | 41+++++++++++++++++++++++++++++++++++++++++
MiosApp/iosApp.xcodeproj/project.pbxproj | 13+++++--------
12 files changed, 245 insertions(+), 104 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -8,3 +8,8 @@ iosApp/*.xcworkspace iosApp/*.xcodeproj/xcuserdata iosApp/Pods/ *.xcuserstate +iosApp/fastlane/report.xml +iosApp/fastlane/README.md +iosApp/fastlane/test_output/ +*.ipa +*.dSYM.zip diff --git a/composeApp/src/androidMain/kotlin/ubi2/sonodrill/model/android_platform.kt b/composeApp/src/androidMain/kotlin/ubi2/sonodrill/model/android_platform.kt @@ -3,6 +3,6 @@ package ubi2.sonodrill.model actual fun current_time_ms(): Long = System.currentTimeMillis() actual fun generate_uuid(): String = java.util.UUID.randomUUID().toString() actual fun file_exists(path: String): Boolean = java.io.File(path).exists() -actual fun open_url(url: String) { - // No-op: needs Activity context, will implement if needed -} +actual fun open_url(url: String) {} +actual fun get_play_count(): Int = 0 +actual fun increment_play_count() {} diff --git a/composeApp/src/commonMain/kotlin/ubi2/sonodrill/model/platform.kt b/composeApp/src/commonMain/kotlin/ubi2/sonodrill/model/platform.kt @@ -4,3 +4,5 @@ expect fun current_time_ms(): Long expect fun generate_uuid(): String expect fun file_exists(path: String): Boolean expect fun open_url(url: String) +expect fun get_play_count(): Int +expect fun increment_play_count() diff --git a/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/app.kt b/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/app.kt @@ -9,6 +9,7 @@ import ubi2.sonodrill.audio.AudioPlayer import ubi2.sonodrill.data.AudioRepository import ubi2.sonodrill.model.AudioFileInfo import ubi2.sonodrill.model.file_exists +import ubi2.sonodrill.model.get_play_count import ubi2.sonodrill.ui.library.LibraryScreen import ubi2.sonodrill.ui.player.PlayerScreen import ubi2.sonodrill.ui.player.PlayerViewModel @@ -49,6 +50,7 @@ fun SonodrillApp(player: AudioPlayer, repo: AudioRepository, on_import: (on_done }, on_import = { on_import { refresh_files() } }, on_delete = { id -> repo.delete_file(id); refresh_files() }, + show_kofi = get_play_count() > 5, ) Screen.PLAYER -> player_vm?.let { vm -> PlayerScreen(vm = vm, on_back = { vm.release(); player_vm = null; refresh_files(); screen = Screen.LIBRARY }) diff --git a/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/library/library_screen.kt b/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/library/library_screen.kt @@ -26,6 +26,7 @@ fun LibraryScreen( on_file_click: (AudioFileInfo) -> Unit, on_import: () -> Unit, on_delete: (String) -> Unit, + show_kofi: Boolean = false, ) { var search by remember { mutableStateOf("") } val filtered = if (search.isBlank()) files else files.filter { it.title.contains(search, ignoreCase = true) } @@ -35,8 +36,10 @@ fun LibraryScreen( TopAppBar( title = { Text("Sonodrill") }, actions = { - IconButton(onClick = { open_url("https://ko-fi.com/jeanschurger") }) { - Icon(Icons.Default.Favorite, "Support on Ko-fi", tint = MaterialTheme.colorScheme.error) + if (show_kofi) { + IconButton(onClick = { open_url("https://ko-fi.com/jeanschurger") }) { + Icon(Icons.Default.Favorite, "Support on Ko-fi", tint = MaterialTheme.colorScheme.error) + } } }, ) diff --git a/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_screen.kt b/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_screen.kt @@ -3,7 +3,10 @@ package ubi2.sonodrill.ui.player import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -178,6 +181,9 @@ fun PlayerScreen(vm: PlayerViewModel, on_back: () -> Unit) { } } +private const val BASE_ROWS = 6 +private const val ROW_HEIGHT_DP = 56 + @Composable private fun ProgressTimeline( position_ms: Long, @@ -193,109 +199,174 @@ private fun ProgressTimeline( val mark_color = Color(0xFFFF6B35) val loop_color = MaterialTheme.colorScheme.primaryContainer - val rows = 6 + var zoom by remember { mutableFloatStateOf(1f) } + val rows = (BASE_ROWS * zoom).toInt().coerceAtLeast(BASE_ROWS) val row_duration = if (duration_ms > 0) duration_ms.toFloat() / rows else 1f val samples_per_row = if (waveform.isNotEmpty()) waveform.size.toFloat() / rows else 0f - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - for (row in 0 until rows) { - val row_start_ms = row * row_duration - val row_end_ms = (row + 1) * row_duration - val row_progress = when { - position_ms >= row_end_ms -> 1f - position_ms <= row_start_ms -> 0f - else -> (position_ms - row_start_ms) / (row_end_ms - row_start_ms) + val list_state = rememberLazyListState() + val is_zoomed = rows > BASE_ROWS + + // Auto-scroll to keep playhead visible + val active_row = if (duration_ms > 0) ((position_ms.toFloat() / duration_ms) * rows).toInt().coerceIn(0, rows - 1) else 0 + LaunchedEffect(active_row, rows) { + if (is_zoomed) { + val visible = list_state.layoutInfo.visibleItemsInfo + val first_visible = visible.firstOrNull()?.index ?: 0 + val last_visible = visible.lastOrNull()?.index ?: 0 + if (active_row < first_visible || active_row > last_visible) { + list_state.animateScrollToItem((active_row - 2).coerceAtLeast(0)) } - val row_marks = marks.filter { it.position_ms >= row_start_ms && it.position_ms < row_end_ms } - val is_active_row = position_ms >= row_start_ms && position_ms < row_end_ms - val wave_start = (row * samples_per_row).toInt() - val wave_end = ((row + 1) * samples_per_row).toInt().coerceAtMost(waveform.size) + } + } - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .clip(RoundedCornerShape(6.dp)) - .background(surface_variant) - .pointerInput(row, duration_ms) { - detectTapGestures { offset -> - val fraction = (offset.x / size.width).coerceIn(0f, 1f) - val tap_ms = (row_start_ms + fraction * (row_end_ms - row_start_ms)).toLong() - on_seek(tap_ms) - } - } + Box( + modifier = Modifier.fillMaxSize().pointerInput(Unit) { + detectTransformGestures { _, _, gestureZoom, _ -> + zoom = (zoom * gestureZoom).coerceIn(1f, 20f) + } + } + ) { + if (is_zoomed) { + // Scrollable when zoomed + LazyColumn( + state = list_state, + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Canvas(modifier = Modifier.fillMaxSize()) { - val w = size.width - val h = size.height - val mid_y = h / 2f - - // Loop region highlight - if (loop_state.enabled && loop_state.mark_a != null && loop_state.mark_b != null) { - val a = loop_state.mark_a!!.position_ms.toFloat() - val b = loop_state.mark_b!!.position_ms.toFloat() - val ls = ((a - row_start_ms) / (row_end_ms - row_start_ms)).coerceIn(0f, 1f) - val le = ((b - row_start_ms) / (row_end_ms - row_start_ms)).coerceIn(0f, 1f) - if (le > ls && a < row_end_ms && b > row_start_ms) { - drawRect( - color = loop_color, - topLeft = Offset(ls * w, 0f), - size = androidx.compose.ui.geometry.Size((le - ls) * w, h), - ) - } - } + items(rows) { row -> + TimelineRow( + row = row, row_duration = row_duration, samples_per_row = samples_per_row, + position_ms = position_ms, duration_ms = duration_ms, marks = marks, + loop_state = loop_state, waveform = waveform, on_seek = on_seek, + primary = primary, surface_variant = surface_variant, on_surface = on_surface, + mark_color = mark_color, loop_color = loop_color, + modifier = Modifier.fillMaxWidth().height(ROW_HEIGHT_DP.dp), + ) + } + } + } else { + // Fixed layout when not zoomed + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + for (row in 0 until rows) { + TimelineRow( + row = row, row_duration = row_duration, samples_per_row = samples_per_row, + position_ms = position_ms, duration_ms = duration_ms, marks = marks, + loop_state = loop_state, waveform = waveform, on_seek = on_seek, + primary = primary, surface_variant = surface_variant, on_surface = on_surface, + mark_color = mark_color, loop_color = loop_color, + modifier = Modifier.fillMaxWidth().weight(1f), + ) + } + } + } + } +} - // Waveform or simple progress - val row_samples = wave_end - wave_start - if (row_samples > 0) { - val bar_width = w / row_samples - for (i in 0 until row_samples) { - val amp = waveform[wave_start + i].coerceIn(0f, 1f) - val bar_h = amp * h * 0.9f - val x = i * bar_width - val frac = i.toFloat() / row_samples - val color = if (frac <= row_progress) primary else primary.copy(alpha = 0.3f) - drawRect( - color = color, - topLeft = Offset(x, mid_y - bar_h / 2f), - size = androidx.compose.ui.geometry.Size(bar_width.coerceAtLeast(1f), bar_h.coerceAtLeast(1f)), - ) - } - } else if (row_progress > 0f) { - drawRect( - color = primary.copy(alpha = 0.3f), - topLeft = Offset.Zero, - size = androidx.compose.ui.geometry.Size(row_progress * w, h), - ) - } +@Composable +private fun TimelineRow( + row: Int, row_duration: Float, samples_per_row: Float, + position_ms: Long, duration_ms: Long, marks: List<MarkInfo>, + loop_state: LoopState, waveform: FloatArray, on_seek: (Long) -> Unit, + primary: Color, surface_variant: Color, on_surface: Color, + mark_color: Color, loop_color: Color, + modifier: Modifier, +) { + val row_start_ms = row * row_duration + val row_end_ms = (row + 1) * row_duration + val row_progress = when { + position_ms >= row_end_ms -> 1f + position_ms <= row_start_ms -> 0f + else -> (position_ms - row_start_ms) / (row_end_ms - row_start_ms) + } + val row_marks = marks.filter { it.position_ms >= row_start_ms && it.position_ms < row_end_ms } + val is_active_row = position_ms >= row_start_ms && position_ms < row_end_ms + val wave_start = (row * samples_per_row).toInt() + val wave_end = ((row + 1) * samples_per_row).toInt().coerceAtMost(waveform.size) - // Marks as vertical lines - for (mark in row_marks) { - val mark_frac = (mark.position_ms - row_start_ms) / (row_end_ms - row_start_ms) - val x = mark_frac * w - drawLine(color = mark_color, start = Offset(x, 0f), end = Offset(x, h), strokeWidth = 3f) - drawCircle(color = mark_color, radius = 5f, center = Offset(x, 5f)) - } + Box( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(surface_variant) + .pointerInput(row, duration_ms) { + detectTapGestures { offset -> + val fraction = (offset.x / size.width).coerceIn(0f, 1f) + val tap_ms = (row_start_ms + fraction * (row_end_ms - row_start_ms)).toLong() + on_seek(tap_ms) + } + } + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + val w = size.width + val h = size.height + val mid_y = h / 2f - // Playhead cursor - if (is_active_row) { - val cursor_x = row_progress * w - drawLine(color = on_surface, start = Offset(cursor_x, 0f), end = Offset(cursor_x, h), strokeWidth = 2.5f) - } + // Loop region highlight + if (loop_state.enabled && loop_state.mark_a != null && loop_state.mark_b != null) { + val a = loop_state.mark_a!!.position_ms.toFloat() + val b = loop_state.mark_b!!.position_ms.toFloat() + val ls = ((a - row_start_ms) / (row_end_ms - row_start_ms)).coerceIn(0f, 1f) + val le = ((b - row_start_ms) / (row_end_ms - row_start_ms)).coerceIn(0f, 1f) + if (le > ls && a < row_end_ms && b > row_start_ms) { + drawRect( + color = loop_color, + topLeft = Offset(ls * w, 0f), + size = androidx.compose.ui.geometry.Size((le - ls) * w, h), + ) } + } - // Row time label - Text( - text = format_time(row_start_ms.toLong()), - fontSize = 10.sp, - color = on_surface.copy(alpha = 0.5f), - modifier = Modifier.padding(start = 4.dp, top = 2.dp), + // Waveform or simple progress + val row_samples = wave_end - wave_start + if (row_samples > 0) { + val bar_width = w / row_samples + for (i in 0 until row_samples) { + val amp = waveform[wave_start + i].coerceIn(0f, 1f) + val bar_h = amp * h * 0.9f + val x = i * bar_width + val frac = (i + 1).toFloat() / row_samples + val color = if (frac <= row_progress) primary else primary.copy(alpha = 0.3f) + drawRect( + color = color, + topLeft = Offset(x, mid_y - bar_h / 2f), + size = androidx.compose.ui.geometry.Size(bar_width.coerceAtLeast(1f), bar_h.coerceAtLeast(1f)), + ) + } + } else if (row_progress > 0f) { + drawRect( + color = primary.copy(alpha = 0.3f), + topLeft = Offset.Zero, + size = androidx.compose.ui.geometry.Size(row_progress * w, h), ) } + + // Marks as vertical lines + for (mark in row_marks) { + val mark_frac = (mark.position_ms - row_start_ms) / (row_end_ms - row_start_ms) + val x = mark_frac * w + drawLine(color = mark_color, start = Offset(x, 0f), end = Offset(x, h), strokeWidth = 3f) + drawCircle(color = mark_color, radius = 5f, center = Offset(x, 5f)) + } + + // Playhead cursor + if (is_active_row) { + val cursor_x = row_progress * w + drawLine(color = on_surface, start = Offset(cursor_x, 0f), end = Offset(cursor_x, h), strokeWidth = 2.5f) + } } + + // Row time label with background for contrast + Text( + text = format_time(row_start_ms.toLong()), + fontSize = 10.sp, + color = on_surface, + modifier = Modifier.padding(start = 2.dp, top = 1.dp) + .background(surface_variant.copy(alpha = 0.7f), RoundedCornerShape(3.dp)) + .padding(horizontal = 3.dp, vertical = 1.dp), + ) } } diff --git a/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_view_model.kt b/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_view_model.kt @@ -9,6 +9,7 @@ import ubi2.sonodrill.model.LoopState import ubi2.sonodrill.model.MarkInfo import ubi2.sonodrill.model.current_time_ms import ubi2.sonodrill.model.generate_uuid +import ubi2.sonodrill.model.increment_play_count import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -38,12 +39,13 @@ class PlayerViewModel( fun load_file(file: AudioFileInfo) { _current_file.value = file player.load(file.uri) + increment_play_count() _marks.value = repo.get_marks(file.id) if (file.last_position_ms > 0) player.seek(file.last_position_ms) if (file.last_speed != 1f) player.set_speed(file.last_speed) scope.launch { val dur = duration.first { it > 0 } - _waveform.value = extract_waveform(file.uri, (dur / 100).toInt().coerceAtLeast(1)) + _waveform.value = extract_waveform(file.uri, (dur / 20).toInt().coerceAtLeast(1)) } start_auto_save() } diff --git a/composeApp/src/iosMain/kotlin/ubi2/sonodrill/audio/ios_audio_player.kt b/composeApp/src/iosMain/kotlin/ubi2/sonodrill/audio/ios_audio_player.kt @@ -96,7 +96,14 @@ class IosAudioPlayer : AudioPlayer { val node = player_node ?: return; val file = audio_file ?: return val time = node.lastRenderTime ?: return; val pt = node.playerTimeForNodeTime(time) ?: return val elapsed_ms = ((pt.sampleTime.toDouble() / file.processingFormat.sampleRate) * 1000).toLong() - _position.value = seek_offset_ms + elapsed_ms + val pos = seek_offset_ms + elapsed_ms + if (pos >= _duration.value) { + _position.value = _duration.value + _playing.value = false + stop_position_updates() + return + } + _position.value = pos } } diff --git a/composeApp/src/iosMain/kotlin/ubi2/sonodrill/model/ios_platform.kt b/composeApp/src/iosMain/kotlin/ubi2/sonodrill/model/ios_platform.kt @@ -11,3 +11,11 @@ actual fun open_url(url: String) { val nsurl = platform.Foundation.NSURL.URLWithString(url) ?: return platform.UIKit.UIApplication.sharedApplication.openURL(nsurl, options = emptyMap<Any?, Any>(), completionHandler = null) } + +private const val PLAY_COUNT_KEY = "sonodrill_play_count" +actual fun get_play_count(): Int = + platform.Foundation.NSUserDefaults.standardUserDefaults.integerForKey(PLAY_COUNT_KEY).toInt() +actual fun increment_play_count() { + val defaults = platform.Foundation.NSUserDefaults.standardUserDefaults + defaults.setInteger((defaults.integerForKey(PLAY_COUNT_KEY) + 1), PLAY_COUNT_KEY) +} diff --git a/iosApp/fastlane/Appfile b/iosApp/fastlane/Appfile @@ -0,0 +1,3 @@ +app_identifier("ubi2.sonodrill") +apple_id(ENV["UBI2_APPLE_ID"]) +team_id("8CH9CCT4MH") diff --git a/iosApp/fastlane/Fastfile b/iosApp/fastlane/Fastfile @@ -0,0 +1,41 @@ +default_platform(:ios) + +platform :ios do + desc "Build Kotlin framework and archive for App Store" + lane :release do + # Build Kotlin framework first + Dir.chdir("..") do + sh("cd .. && ./gradlew :composeApp:assembleReleaseAppleFrameworkForXcode " \ + "-PXCODE_CONFIGURATION=Release " \ + "-PXCODE_SDK=iphoneos " \ + "-PXCODE_ARCHS=arm64") + end + + # Regenerate Xcode project + sh("cd .. && xcodegen generate") + + api_key = app_store_connect_api_key( + key_id: ENV["UBI2_ASC_KEY_ID"], + issuer_id: ENV["UBI2_ASC_ISSUER_ID"], + key_filepath: ENV["UBI2_ASC_KEY_PATH"], + ) + + increment_build_number( + xcodeproj: "iosApp.xcodeproj", + build_number: Time.now.strftime("%Y%m%d%H%M"), + ) + + build_app( + project: "iosApp.xcodeproj", + scheme: "iosApp", + export_method: "app-store", + destination: "generic/platform=iOS", + xcargs: "-allowProvisioningUpdates", + ) + + upload_to_testflight( + api_key: api_key, + skip_waiting_for_build_processing: true, + ) + end +end diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -19,7 +19,7 @@ 245434685F34A9A15B1AC406 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 2BCF6AAC471512DFD0FA06EE /* WaveformExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformExtractor.swift; sourceTree = "<group>"; }; 8395CD5B070D5A2FB485F0DA /* iosApp.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = iosApp.xcodeproj; sourceTree = "<group>"; }; - 86038AE1032E284E79B32AD6 /* iosApp.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; name = iosApp.app; path = Sonodrill.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 86038AE1032E284E79B32AD6 /* Sonodrill.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sonodrill.app; sourceTree = BUILT_PRODUCTS_DIR; }; CE8509E5A49E4A5CAAEB3ADB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; E74C38E65621F5192E1B68D2 /* SonodrillApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SonodrillApp.swift; sourceTree = "<group>"; }; F543015C899423D4B01CEB2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; @@ -75,7 +75,7 @@ 94C98B8B3A2BFC1966447269 /* Products */ = { isa = PBXGroup; children = ( - 86038AE1032E284E79B32AD6 /* iosApp.app */, + 86038AE1032E284E79B32AD6 /* Sonodrill.app */, ); name = Products; sourceTree = "<group>"; @@ -100,7 +100,7 @@ packageProductDependencies = ( ); productName = iosApp; - productReference = 86038AE1032E284E79B32AD6 /* iosApp.app */; + productReference = 86038AE1032E284E79B32AD6 /* Sonodrill.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -111,11 +111,6 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1430; - TargetAttributes = { - 78B53CC79860CBFC2F5543C6 = { - DevelopmentTeam = ""; - }; - }; }; buildConfigurationList = FF7CA1CA754D017066F1822F /* Build configuration list for PBXProject "iosApp" */; compatibilityVersion = "Xcode 14.0"; @@ -315,6 +310,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 8CH9CCT4MH; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; INFOPLIST_FILE = Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -334,6 +330,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 8CH9CCT4MH; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; INFOPLIST_FILE = Info.plist; LD_RUNPATH_SEARCH_PATHS = (