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 0000000..fb53efa Binary files /dev/null and b/MusicBridge/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/MusicBridge/Assets.xcassets/AppIcon.appiconset/Contents.json b/MusicBridge/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..0a42994 --- /dev/null +++ b/MusicBridge/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"AppIcon.png","idiom":"universal","platform":"ios","size":"1024x1024"}],"info":{"author":"xcode","version":1}} diff --git a/MusicBridge/Assets.xcassets/Contents.json b/MusicBridge/Assets.xcassets/Contents.json new file mode 100644 index 0000000..f223229 --- /dev/null +++ b/MusicBridge/Assets.xcassets/Contents.json @@ -0,0 +1 @@ +{"info":{"author":"xcode","version":1}} diff --git a/MusicBridge/ContentView.swift b/MusicBridge/ContentView.swift index 0bf7fb7..d0f8d98 100644 --- a/MusicBridge/ContentView.swift +++ b/MusicBridge/ContentView.swift @@ -7,29 +7,47 @@ struct ContentView: View { var body: some View { NavigationStack { ScrollView { - VStack(spacing: 24) { - Label(store.status, systemImage: store.connected ? "wifi" : "wifi.slash") - .font(.subheadline).foregroundStyle(.secondary) + VStack(spacing: 20) { + HStack(spacing: 8) { + Circle().fill(store.connected ? Color.green : Color.secondary).frame(width: 7, height: 7) + Text(store.status).font(.subheadline).foregroundStyle(.secondary) + Spacer(minLength: 0) + }.accessibilityElement(children: .combine) if store.connection == nil { PairingView(store: store, discovery: store.discovery) } else { PlayerView(store: store) - Text(store.connection?.endpoint.host ?? "") - .font(.caption).foregroundStyle(.secondary) + if !store.connected && !store.needsPairing { Button("Подключиться снова") { store.resume() } } - Button("Забыть компьютер", role: .destructive) { showForget = true } - .font(.footnote) + if store.needsPairing { + Button("Выполнить сопряжение заново") { showForget = true } + .buttonStyle(.borderedProminent).controlSize(.large) + } } if let error = store.error { Text(error).font(.callout).foregroundStyle(.red).textSelection(.enabled) Button("Скрыть сообщение") { store.error = nil }.font(.footnote) } - }.padding(24) + }.padding(20) } + .scrollDismissesKeyboard(.interactively) .background(Color(.systemGroupedBackground)) .navigationTitle("MusicBridge") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + if store.connection != nil { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Text(store.connection?.endpoint.host ?? "Компьютер") + Button("Подключиться снова", systemImage: "arrow.clockwise") { store.resume() }.disabled(store.needsPairing) + Button("Забыть компьютер", systemImage: "trash", role: .destructive) { showForget = true } + } label: { Image(systemName: "desktopcomputer").frame(minWidth: 48, minHeight: 48) } + .accessibilityLabel("Настройки компьютера") + } + } + } .confirmationDialog("Забыть этот компьютер?", isPresented: $showForget, titleVisibility: .visible) { Button("Забыть", role: .destructive) { store.forget() } } message: { @@ -39,6 +57,12 @@ struct ContentView: View { } } +private extension View { + func bridgeCard() -> 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))