From cbd3c0f6f4c340b4899f4a234e218d7211537b51 Mon Sep 17 00:00:00 2001 From: ares Date: Sat, 12 Sep 2026 21:54:23 +0300 Subject: [PATCH] Refresh native pairing and player UI and add the MusicBridge app icon --- .github/workflows/ios.yml | 20 ++- MusicBridge.xcodeproj/project.pbxproj | 36 ++++- .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 94489 bytes .../AppIcon.appiconset/Contents.json | 1 + MusicBridge/Assets.xcassets/Contents.json | 1 + MusicBridge/ContentView.swift | 124 ++++++++++++++---- MusicBridge/MusicBridgeApp.swift | 7 +- MusicBridge/PreviewSupport.swift | 37 ++++++ tools/generate_icon.ps1 | 25 ++++ tools/generate_project.py | 7 +- 10 files changed, 216 insertions(+), 42 deletions(-) create mode 100644 MusicBridge/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100644 MusicBridge/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 MusicBridge/Assets.xcassets/Contents.json create mode 100644 MusicBridge/PreviewSupport.swift create mode 100644 tools/generate_icon.ps1 diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index db96faa..193f9ef 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -75,11 +75,29 @@ jobs: xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge sleep 3 xcrun simctl io "$SIMULATOR_ID" screenshot build/pairing-screen.png + xcrun simctl terminate "$SIMULATOR_ID" ru.yukinoki.musicbridge + xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge --preview-player + sleep 2 + xcrun simctl io "$SIMULATOR_ID" screenshot build/player-light-screen.png + xcrun simctl ui "$SIMULATOR_ID" appearance dark + sleep 1 + xcrun simctl io "$SIMULATOR_ID" screenshot build/player-dark-screen.png + xcrun simctl terminate "$SIMULATOR_ID" ru.yukinoki.musicbridge + xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge --preview-player --preview-large-text + sleep 2 + xcrun simctl io "$SIMULATOR_ID" screenshot build/player-large-text-screen.png + RUNTIME=$(python3 -c 'import json,os; d=json.load(open("/tmp/musicbridge-simulators.json")); print(next(k for k,v in d["devices"].items() if any(x["udid"]==os.environ["SIMULATOR_ID"] for x in v)))') + COMPACT_ID=$(xcrun simctl create MusicBridge-Compact com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation "$RUNTIME") + xcrun simctl bootstatus "$COMPACT_ID" -b + xcrun simctl install "$COMPACT_ID" DerivedData/Build/Products/Debug-iphonesimulator/MusicBridge.app + xcrun simctl launch "$COMPACT_ID" ru.yukinoki.musicbridge --preview-player + sleep 3 + xcrun simctl io "$COMPACT_ID" screenshot build/player-compact-screen.png - name: Save pairing screen uses: actions/upload-artifact@v4 with: name: ios-ui-preview - path: build/pairing-screen.png + path: build/*-screen.png retention-days: 7 - name: Save simulator application uses: actions/upload-artifact@v4 diff --git a/MusicBridge.xcodeproj/project.pbxproj b/MusicBridge.xcodeproj/project.pbxproj index 0b510d1..081d83f 100644 --- a/MusicBridge.xcodeproj/project.pbxproj +++ b/MusicBridge.xcodeproj/project.pbxproj @@ -85,6 +85,16 @@ "isa" = "PBXBuildFile"; "fileRef" = "9C432A0DA0BEC173839EBF86"; }; + "6B0F2027221CDD63E8FD4C59" = { + "isa" = "PBXFileReference"; + "lastKnownFileType" = "sourcecode.swift"; + "path" = "PreviewSupport.swift"; + "sourceTree" = ""; + }; + "287044ADBD143F929035BC61" = { + "isa" = "PBXBuildFile"; + "fileRef" = "6B0F2027221CDD63E8FD4C59"; + }; "7148B52E915554F67E875C26" = { "isa" = "PBXFileReference"; "lastKnownFileType" = "sourcecode.swift"; @@ -99,7 +109,17 @@ "isa" = "PBXGroup"; "path" = "MusicBridge"; "sourceTree" = ""; - "children" = ("065A9D559DC80AAF876DA757", "763FA6A137E5E94274AC56A3", "971C0AE92F167DE7AB145086", "5F6F408829AB46D3B85FB911", "76FB6BFFC478599567424755", "124CF3F5AC075D1D6042DF64", "ADF0CD56882201BD3AC1581B", "9C432A0DA0BEC173839EBF86", "7148B52E915554F67E875C26",); + "children" = ("065A9D559DC80AAF876DA757", "763FA6A137E5E94274AC56A3", "971C0AE92F167DE7AB145086", "5F6F408829AB46D3B85FB911", "76FB6BFFC478599567424755", "124CF3F5AC075D1D6042DF64", "ADF0CD56882201BD3AC1581B", "9C432A0DA0BEC173839EBF86", "6B0F2027221CDD63E8FD4C59", "7148B52E915554F67E875C26", "8BF729FFE074CAEE622C0292",); + }; + "8BF729FFE074CAEE622C0292" = { + "isa" = "PBXFileReference"; + "lastKnownFileType" = "folder.assetcatalog"; + "path" = "Assets.xcassets"; + "sourceTree" = ""; + }; + "5522B3B3B2EACC1C8F9BEAAC" = { + "isa" = "PBXBuildFile"; + "fileRef" = "8BF729FFE074CAEE622C0292"; }; "C9F705D432CA45B6056BC572" = { "isa" = "PBXFileReference"; @@ -177,7 +197,7 @@ "72B74C8A2143F64FD53D71E5" = { "isa" = "PBXSourcesBuildPhase"; "buildActionMask" = 2147483647; - "files" = ("82F1C33CE4F8B8F641FF0461", "40D2920E2C996D62B7190BA4", "16EA630DE1FE9F0A16580C9D", "842E87CD23DCB5BBCA4340F6", "DA06B3B12E52FB69A6A34887", "DEDCCD95A140162B6D654871", "48E2FDCA11A6694953C86E6D", "47693934E8A974162CF08A38", "BE808A0D13A16FE67D32B7FB",); + "files" = ("82F1C33CE4F8B8F641FF0461", "40D2920E2C996D62B7190BA4", "16EA630DE1FE9F0A16580C9D", "842E87CD23DCB5BBCA4340F6", "DA06B3B12E52FB69A6A34887", "DEDCCD95A140162B6D654871", "48E2FDCA11A6694953C86E6D", "47693934E8A974162CF08A38", "287044ADBD143F929035BC61", "BE808A0D13A16FE67D32B7FB",); "runOnlyForDeploymentPostprocessing" = 0; }; "734C4650C7A5604092763C32" = { @@ -189,7 +209,7 @@ "69816CB820E8173702C54C37" = { "isa" = "PBXResourcesBuildPhase"; "buildActionMask" = 2147483647; - "files" = (); + "files" = ("5522B3B3B2EACC1C8F9BEAAC",); "runOnlyForDeploymentPostprocessing" = 0; }; "E3322BA79DBD9A84650B2CAE" = { @@ -206,8 +226,9 @@ "CODE_SIGN_STYLE" = "Automatic"; "GENERATE_INFOPLIST_FILE" = "NO"; "INFOPLIST_FILE" = "MusicBridge/Info.plist"; - "CURRENT_PROJECT_VERSION" = "4"; - "MARKETING_VERSION" = "0.3.0"; + "CURRENT_PROJECT_VERSION" = "5"; + "MARKETING_VERSION" = "0.4.0"; + "ASSETCATALOG_COMPILER_APPICON_NAME" = "AppIcon"; "SWIFT_OPTIMIZATION_LEVEL" = "-Onone"; "ENABLE_TESTABILITY" = "YES"; "SWIFT_ACTIVE_COMPILATION_CONDITIONS" = "DEBUG"; @@ -227,8 +248,9 @@ "CODE_SIGN_STYLE" = "Automatic"; "GENERATE_INFOPLIST_FILE" = "NO"; "INFOPLIST_FILE" = "MusicBridge/Info.plist"; - "CURRENT_PROJECT_VERSION" = "4"; - "MARKETING_VERSION" = "0.3.0"; + "CURRENT_PROJECT_VERSION" = "5"; + "MARKETING_VERSION" = "0.4.0"; + "ASSETCATALOG_COMPILER_APPICON_NAME" = "AppIcon"; "SWIFT_OPTIMIZATION_LEVEL" = "-O"; }; }; diff --git a/MusicBridge/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/MusicBridge/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..fb53efac4abbd7199404a14ee3a25fe28e8a5816 GIT binary patch literal 94489 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

qhOo*hLQu!klsC8M}(;W-2_X|4S?Q3iEe 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 usq some View { + padding(18).background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 24)) + } +} + private struct PairingView: View { @ObservedObject var store: PlayerStore @ObservedObject var discovery: DiscoveryStore @@ -47,22 +71,34 @@ private struct PairingView: View { @State private var code = "" @State private var invitation = "" @State private var showScanner = false + @State private var showDetails = false + @State private var prepared = false + @FocusState private var editing: Bool var body: some View { VStack(alignment: .leading, spacing: 18) { - Image(systemName: "desktopcomputer.and.arrow.down") - .font(.system(size: 48)).foregroundStyle(.indigo) - .frame(maxWidth: .infinity).accessibilityHidden(true) - Text("Музыка на ПК. Управление здесь.").font(.title2.bold()) - Text("Откройте сопряжение в MusicBridge на Windows. Подключите iPhone к той же домашней сети.") - .foregroundStyle(.secondary) - Button { showScanner = true } label: { - Label("Сканировать QR с ПК", systemImage: "qrcode.viewfinder") + VStack(spacing: 14) { + Image(systemName: "waveform.path") + .font(.system(size: 40, weight: .medium)).foregroundStyle(.white) + .frame(width: 84, height: 84) + .background(LinearGradient(colors: [.indigo, .teal], startPoint: .topLeading, endPoint: .bottomTrailing), in: RoundedRectangle(cornerRadius: 25)) + .accessibilityHidden(true) + Text("Ваш ПК. Ваша музыка.").font(.title2.bold()).multilineTextAlignment(.center) + Text("Откройте QR в агенте на компьютере. iPhone и ПК должны быть в одной сети.") + .font(.subheadline).foregroundStyle(.secondary).multilineTextAlignment(.center) + Button { editing = false; showScanner = true } label: { + Label("Сканировать QR", systemImage: "qrcode.viewfinder") .frame(maxWidth: .infinity) }.buttonStyle(.borderedProminent).controlSize(.large).disabled(store.pairing) - Text("QR заполнит адрес, отпечаток и код. Затем нажмите «Подключить компьютер».") - .font(.caption).foregroundStyle(.secondary) - DisclosureGroup("Найти ПК в сети") { + }.frame(maxWidth: .infinity).bridgeCard() + if prepared { + Label("QR прочитан · данные готовы", systemImage: "checkmark.circle.fill") + .font(.subheadline.weight(.semibold)).foregroundStyle(.teal) + Text(endpoint).font(.footnote.monospaced()).textSelection(.enabled) + Text("Если приглашение пришло не с экрана вашего ПК, сверьте отпечаток в деталях подключения.") + .font(.caption).foregroundStyle(.secondary) + } + DisclosureGroup("Найти компьютер") { VStack(alignment: .leading, spacing: 12) { Text(discovery.message).font(.footnote).foregroundStyle(.secondary) ForEach(discovery.computers) { computer in @@ -70,6 +106,7 @@ private struct PairingView: View { endpoint = computer.endpoint fingerprint = computer.fingerprint code = "" + showDetails = true; prepared = false store.error = nil } label: { VStack(alignment: .leading) { @@ -85,15 +122,24 @@ private struct PairingView: View { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } } }.padding(.top, 8) - }.disabled(store.pairing) + }.bridgeCard().disabled(store.pairing) + DisclosureGroup(prepared ? "Детали подключения" : "Подключить вручную", isExpanded: $showDetails) { + VStack(alignment: .leading, spacing: 12) { Group { + Text("Адрес компьютера").font(.subheadline.weight(.medium)) TextField("Адрес ПК: https://192.168.1.10:8765", text: $endpoint).keyboardType(.URL) + Text("Отпечаток сертификата").font(.subheadline.weight(.medium)) TextField("SHA-256 сертификата с экрана ПК", text: $fingerprint, axis: .vertical) .lineLimit(2...4).font(.system(.footnote, design: .monospaced)) + Text("Код сопряжения").font(.subheadline.weight(.medium)) TextField("Восьмизначный код", text: $code).keyboardType(.numberPad) - }.textFieldStyle(.roundedBorder).textInputAutocapitalization(.never).autocorrectionDisabled() + }.textFieldStyle(.roundedBorder).textInputAutocapitalization(.never).autocorrectionDisabled().focused($editing) Text("Сверьте весь отпечаток с экраном ПК перед подключением.").font(.caption).foregroundStyle(.secondary) + }.padding(.top, 12) + }.bridgeCard().disabled(store.pairing) + if prepared || showDetails { Button { + editing = false Task { await store.pair(endpoint: endpoint.trimmingCharacters(in: .whitespacesAndNewlines), fingerprint: fingerprint.trimmingCharacters(in: .whitespacesAndNewlines), code: code.trimmingCharacters(in: .whitespacesAndNewlines)) } @@ -103,7 +149,8 @@ private struct PairingView: View { Text(store.pairing ? "Сопряжение…" : "Подключить компьютер") }.frame(maxWidth: .infinity) }.buttonStyle(.borderedProminent).controlSize(.large).disabled(store.pairing) - DisclosureGroup("Импорт строки из QR") { + } + DisclosureGroup("Вставить приглашение") { VStack(spacing: 12) { TextField("musicbridge://pair?data=…", text: $invitation, axis: .vertical) .lineLimit(3...5).textInputAutocapitalization(.never).autocorrectionDisabled() @@ -111,7 +158,10 @@ private struct PairingView: View { importText(invitation) } }.padding(.top, 12) - }.disabled(store.pairing) + }.bridgeCard().disabled(store.pairing) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { Spacer(); Button("Готово") { editing = false } } } .sheet(isPresented: $showScanner) { QRScannerSheet(receive: importText) } .onAppear { consumeIncoming() } @@ -130,6 +180,7 @@ private struct PairingView: View { endpoint = parsed.endpoint fingerprint = parsed.certificateSha256 code = parsed.code + prepared = true; showDetails = false; editing = false store.error = nil } catch { store.error = error.localizedDescription } } @@ -137,6 +188,7 @@ private struct PairingView: View { private struct PlayerView: View { @ObservedObject var store: PlayerStore + @Environment(\.dynamicTypeSize) private var textSize @State private var seekValue = 0.0 @State private var seeking = false @State private var volumeValue = 0.0 @@ -148,10 +200,11 @@ private struct PlayerView: View { if let artwork = store.artwork { Image(uiImage: artwork).resizable().scaledToFit() } else { - RoundedRectangle(cornerRadius: 24).fill(.indigo.opacity(0.12)) - .overlay { Image(systemName: "music.note").font(.system(size: 72)).foregroundStyle(.indigo) } + RoundedRectangle(cornerRadius: 28) + .fill(LinearGradient(colors: [.indigo.opacity(0.85), .teal.opacity(0.75)], startPoint: .topLeading, endPoint: .bottomTrailing)) + .overlay { Image(systemName: "waveform.path").font(.system(size: 72, weight: .light)).foregroundStyle(.white.opacity(0.9)) } } - }.frame(height: 270).clipShape(RoundedRectangle(cornerRadius: 24)) + }.frame(maxWidth: textSize.isAccessibilitySize ? 210 : 300).aspectRatio(1, contentMode: .fit).clipShape(RoundedRectangle(cornerRadius: 28)) .accessibilityLabel("Обложка трека") if let error = store.artworkError { Text(error).font(.footnote).foregroundStyle(.secondary) @@ -178,14 +231,21 @@ private struct PlayerView: View { }.font(.caption.monospacedDigit()).foregroundStyle(.secondary) } } - HStack(spacing: 36) { + HStack(spacing: 28) { control("previous", icon: "backward.end.fill", label: "Предыдущий трек", enabled: state.capabilities.previous) let playing = state.playbackStatus == "Playing" let command = state.capabilities.toggle ? "toggle" : (playing ? "pause" : "play") control(command, icon: playing ? "pause.circle.fill" : "play.circle.fill", label: playing ? "Пауза" : "Воспроизвести", - enabled: state.capabilities.toggle || (playing ? state.capabilities.pause : state.capabilities.play), size: 64) + enabled: state.capabilities.toggle || (playing ? state.capabilities.pause : state.capabilities.play), size: 78) control("next", icon: "forward.end.fill", label: "Следующий трек", enabled: state.capabilities.next) } + VStack(spacing: 8) { + HStack { + Text("Громкость ПК").font(.subheadline.weight(.semibold)) + Spacer() + Text(state.muted == true ? "Без звука" : state.volume.map { "\(Int((changingVolume ? volumeValue : $0) * 100))%" } ?? "Недоступна") + .font(.caption.monospacedDigit()).foregroundStyle(.secondary) + } HStack { control("toggleMute", icon: state.muted == true ? "speaker.slash.fill" : "speaker.wave.2.fill", label: state.muted == true ? "Включить звук ПК" : "Выключить звук ПК", enabled: state.muted != nil, size: 22) @@ -195,13 +255,19 @@ private struct PlayerView: View { if !editing { Task { await store.send("volume", value: volumeValue) } } }.disabled(state.volume == nil || store.busy).accessibilityLabel("Системная громкость ПК") } + }.bridgeCard() ForEach([state.mediaError, state.volumeError, state.artworkError].compactMap { $0 }, id: \.self) { Text($0).font(.caption).foregroundStyle(.secondary) } } else { - Text("Ожидаем состояние компьютера").foregroundStyle(.secondary) + VStack(spacing: 8) { + Text(store.needsPairing ? "Подключите ПК заново" : "Ждём ваш компьютер").font(.title2.bold()) + Text("Проверьте, что агент запущен и устройства находятся в одной сети.") + .font(.subheadline).foregroundStyle(.secondary).multilineTextAlignment(.center) + } } }.disabled(!store.connected) + .onChange(of: store.connected) { _, _ in seeking = false; changingVolume = false } } private func control(_ type: String, icon: String, label: String, enabled: Bool, size: CGFloat = 28) -> some View { diff --git a/MusicBridge/MusicBridgeApp.swift b/MusicBridge/MusicBridgeApp.swift index 8a6c331..75265cf 100644 --- a/MusicBridge/MusicBridgeApp.swift +++ b/MusicBridge/MusicBridgeApp.swift @@ -2,15 +2,16 @@ import SwiftUI @main struct MusicBridgeApp: App { - @StateObject private var store = PlayerStore() + @StateObject private var store = PreviewSupport.makeStore() @Environment(\.scenePhase) private var phase var body: some Scene { WindowGroup { ContentView(store: store) + .dynamicTypeSize(PreviewSupport.largeText ? DynamicTypeSize.accessibility2...DynamicTypeSize.accessibility2 : DynamicTypeSize.xSmall...DynamicTypeSize.accessibility5) .onOpenURL { store.importInvitation($0.absoluteString) } - .task { store.setActive(phase == .active) } - .onChange(of: phase) { _, value in store.setActive(value == .active) } + .task { if !PreviewSupport.enabled { store.setActive(phase == .active) } } + .onChange(of: phase) { _, value in if !PreviewSupport.enabled { store.setActive(value == .active) } } } } } diff --git a/MusicBridge/PreviewSupport.swift b/MusicBridge/PreviewSupport.swift new file mode 100644 index 0000000..2fe7483 --- /dev/null +++ b/MusicBridge/PreviewSupport.swift @@ -0,0 +1,37 @@ +import SwiftUI + +// Screenshots only: this fixture and its launch switches do not exist in Release. +@MainActor +enum PreviewSupport { + static var enabled: Bool { + #if DEBUG + ProcessInfo.processInfo.arguments.contains("--preview-player") + #else + false + #endif + } + static var largeText: Bool { + #if DEBUG + ProcessInfo.processInfo.arguments.contains("--preview-large-text") + #else + false + #endif + } + static func makeStore() -> PlayerStore { + #if DEBUG + if enabled { + let store = PlayerStore(storage: ConnectionStorage(load: { nil }, save: { _ in }, delete: {}), discoveryEnabled: false) + store.connection = try? Connection(endpoint: "https://192.168.1.10:8765", fingerprint: String(repeating: "A", count: 64)) + store.connected = true + store.status = "Предпросмотр интерфейса" + store.state = MediaState(protocolVersion: 1, hasSession: true, title: "Ночной город", artist: "MusicBridge · пример плеера", source: "", + playbackStatus: "Playing", positionSeconds: 42, startSeconds: 0, endSeconds: 214, + minSeekSeconds: 0, maxSeekSeconds: 214, playbackRate: 1, + capabilities: Capabilities(play: true, pause: true, toggle: true, next: true, previous: true, seek: true), + volume: 0.64, muted: false, mediaError: nil, volumeError: nil, artworkId: nil, artworkError: nil) + return store + } + #endif + return PlayerStore() + } +} diff --git a/tools/generate_icon.ps1 b/tools/generate_icon.ps1 new file mode 100644 index 0000000..32c13f7 --- /dev/null +++ b/tools/generate_icon.ps1 @@ -0,0 +1,25 @@ +# Original vector artwork, rendered to the opaque 1024px asset required by Xcode. +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName System.Drawing +$iconFolder = Join-Path $PSScriptRoot '..\MusicBridge\Assets.xcassets\AppIcon.appiconset' +[System.IO.Directory]::CreateDirectory($iconFolder) | Out-Null +$bitmap = [System.Drawing.Bitmap]::new(1024, 1024, [System.Drawing.Imaging.PixelFormat]::Format24bppRgb) +$graphics = [System.Drawing.Graphics]::FromImage($bitmap) +$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias +$background = [System.Drawing.Drawing2D.LinearGradientBrush]::new( + [System.Drawing.Point]::new(0, 0), [System.Drawing.Point]::new(1024, 1024), + [System.Drawing.Color]::FromArgb(59, 43, 164), [System.Drawing.Color]::FromArgb(23, 145, 158)) +$graphics.FillRectangle($background, 0, 0, 1024, 1024) +$ink = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::White) +$heights = @(176, 316, 472, 316, 176) +for ($index = 0; $index -lt 5; $index++) { + $barX = 204 + $index * 132 + $barHeight = $heights[$index] + $barY = 512 - $barHeight / 2 + $graphics.FillRectangle($ink, $barX, $barY + 42, 88, $barHeight - 84) + $graphics.FillEllipse($ink, $barX, $barY, 88, 88) + $graphics.FillEllipse($ink, $barX, $barY + $barHeight - 88, 88, 88) +} +$bitmap.Save((Join-Path $iconFolder 'AppIcon.png'), [System.Drawing.Imaging.ImageFormat]::Png) +$ink.Dispose(); $background.Dispose(); $graphics.Dispose(); $bitmap.Dispose() +Write-Output 'Generated original MusicBridge icon: 1024x1024 RGB PNG.' diff --git a/tools/generate_project.py b/tools/generate_project.py index 1c1c494..1bef3e1 100644 --- a/tools/generate_project.py +++ b/tools/generate_project.py @@ -35,6 +35,9 @@ def source_group(folder): return add(folder, "PBXGroup", path=folder, sourceTree="", children=refs), builds app_group, app_sources = source_group("MusicBridge") +assets = add("assets", "PBXFileReference", lastKnownFileType="folder.assetcatalog", path="Assets.xcassets", sourceTree="") +objects[app_group]["children"].append(assets) +assets_build = add("assets-build", "PBXBuildFile", fileRef=assets) test_group, test_sources = source_group("MusicBridgeTests") fixture = add("fixture", "PBXFileReference", lastKnownFileType="text.json", path="state-v1.json", sourceTree="") objects[test_group]["children"].append(fixture) @@ -50,7 +53,7 @@ for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridg products.append(product) phases = [add(name + "sources", "PBXSourcesBuildPhase", buildActionMask=2147483647, files=sources, runOnlyForDeploymentPostprocessing=0), add(name + "frameworks", "PBXFrameworksBuildPhase", buildActionMask=2147483647, files=[], runOnlyForDeploymentPostprocessing=0), - add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[fixture_build, tls_fixture_build] if is_test else [], runOnlyForDeploymentPostprocessing=0)] + add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[fixture_build, tls_fixture_build] if is_test else [assets_build], runOnlyForDeploymentPostprocessing=0)] settings = dict(PRODUCT_NAME="$(TARGET_NAME)", PRODUCT_BUNDLE_IDENTIFIER="ru.yukinoki.musicbridge" + (".tests" if is_test else ""), SWIFT_VERSION="5.0", IPHONEOS_DEPLOYMENT_TARGET="17.0", SDKROOT="iphoneos", SUPPORTED_PLATFORMS="iphoneos iphonesimulator", TARGETED_DEVICE_FAMILY="1", CODE_SIGN_STYLE="Automatic", @@ -61,7 +64,7 @@ for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridg proxy = add("test-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1, remoteGlobalIDString=uid("MusicBridgetarget"), remoteInfo="MusicBridge") deps = [add("test-dependency", "PBXTargetDependency", target=uid("MusicBridgetarget"), targetProxy=proxy)] else: - settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="4", MARKETING_VERSION="0.3.0") + settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="5", MARKETING_VERSION="0.4.0", ASSETCATALOG_COMPILER_APPICON_NAME="AppIcon") targets.append(add(name + "target", "PBXNativeTarget", name=name, productName=name, productReference=product, productType="com.apple.product-type.bundle.unit-test" if is_test else "com.apple.product-type.application", buildConfigurationList=configurations(name, settings), buildPhases=phases, buildRules=[], dependencies=deps))