Author SHA1 Message Date
cdnutterandClaude Sonnet 5.5 75c6d89608 ios 1.0.16: more room between seek bar and time/codec row
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:38:54 -07:00
cdnutterandClaude Opus 5.5 f065c95422 ios: source.json for 1.0.15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:22:36 -07:00
cdnutterandClaude Opus 5.5 c17d6f174b ios: 1.0.15, mixer rows stretch to fill spare height
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:22:27 -07:00
cdnutterandClaude Sonnet 5.5 e7e4fcc9d6 ios: 1.0.14, source.json, README for tab bar, AirPlay and debug logs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:14:55 -07:00
4 changed files with 35 additions and 8 deletions

No files matched your search

+3
View File
@@ -171,6 +171,9 @@ CarPlay and Siri need the `carplay-audio` and `siri` entitlements, which a free
- **Now Playing:** shuffle and repeat sit either side of the play controls; tap repeat again for repeat one. The speed button (next to the mixer) goes from 0.5× to 2× without changing pitch. - **Now Playing:** shuffle and repeat sit either side of the play controls; tap repeat again for repeat one. The speed button (next to the mixer) goes from 0.5× to 2× without changing pitch.
- **Mixer:** on Now Playing, under the waveform. Tap **Save** to keep a mix for that song; it comes back every time the song plays. - **Mixer:** on Now Playing, under the waveform. Tap **Save** to keep a mix for that song; it comes back every time the song plays.
- **AirPlay:** the AirPlay button on Now Playing picks the speaker or TV. Stems are mixed on the phone, so mixer changes apply live while it plays over AirPlay.
- **Tab bar:** Settings → Tab Bar. Pick and reorder up to five of Library, Songs, Albums, Artists, Genres, Playlists, Search, Downloads and Settings. Settings always stays.
- **Debug logs:** Settings → Debug. Turn on Verbose Logging, then View, Export or Clear the log. Export opens the share sheet with the app version, device, iOS version and audio output at the top. Tokens are stripped from the log.
- **Karaoke:** the mic button mutes vocals on every song and shows synced lyrics from [LRCLIB](https://lrclib.net). Tap a line to jump to it. Vocals can be On, Soft or Off, and Soft has a slider for how quiet. - **Karaoke:** the mic button mutes vocals on every song and shows synced lyrics from [LRCLIB](https://lrclib.net). Tap a line to jump to it. Vocals can be On, Soft or Off, and Soft has a slider for how quiet.
- **Playlists:** the Playlists tab switches between Plex and Stem. - **Playlists:** the Playlists tab switches between Plex and Stem.
- Plex playlists sync with Plex and Plexamp. - Plex playlists sync with Plex and Plexamp.
+12 -6
View File
@@ -457,7 +457,7 @@ struct ProgressVolumeBar: View {
TimelineView(.animation(paused: !pm.playWhenReady || scrub != nil)) { _ in TimelineView(.animation(paused: !pm.playWhenReady || scrub != nil)) { _ in
let pos = scrub.map { Int64($0 * Double(dur)) } ?? pm.positionMs() let pos = scrub.map { Int64($0 * Double(dur)) } ?? pm.positionMs()
let frac = dur > 0 ? min(max(Double(pos) / Double(dur), 0), 1) : 0 let frac = dur > 0 ? min(max(Double(pos) / Double(dur), 0), 1) : 0
VStack(spacing: 4) { VStack(spacing: 12) {
Canvas { ctx, size in Canvas { ctx, size in
let barW = 2.7, pitch = 4.6 let barW = 2.7, pitch = 4.6
let count = max(8, Int(size.width / pitch)) let count = max(8, Int(size.width / pitch))
@@ -583,6 +583,9 @@ private struct MixerPanel: View {
let pm = g.playback let pm = g.playback
let names = pm.stemNames let names = pm.stemNames
let stems = pm.source == .Stems let stems = pm.source == .Stems
GeometryReader { geo in
// Spread spare height across the rows (stems + master) instead of leaving it all at the bottom.
let rowH = min(62, max(46, (geo.size.height - 80) / CGFloat(names.count + 1)))
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
if pm.source == .Original { if pm.source == .Original {
@@ -622,21 +625,22 @@ private struct MixerPanel: View {
let levels = stems ? meters.step(pm.engine, names.count) : [] let levels = stems ? meters.step(pm.engine, names.count) : []
VStack(spacing: 0) { VStack(spacing: 0) {
ForEach(Array(names.enumerated()), id: \.element) { i, name in ForEach(Array(names.enumerated()), id: \.element) { i, name in
StemFader(name: name, setting: pm.mix[name] ?? StemSetting(), level: i < levels.count ? levels[i] : 0, enabled: stems) StemFader(name: name, setting: pm.mix[name] ?? StemSetting(), level: i < levels.count ? levels[i] : 0, enabled: stems, height: rowH)
} }
} }
} }
p.separator.frame(height: 0.5).padding(.vertical, 6) p.separator.frame(height: 0.5).padding(.vertical, 6)
FaderRow(dot: nil, label: "Master", percent: Int(pm.master * 100), dim: false) { FaderRow(dot: nil, label: "Master", percent: Int(pm.master * 100), dim: false, height: rowH) {
FaderSlider(value: pm.master, fill: p.label, onChange: pm.setMaster) FaderSlider(value: pm.master, fill: p.label, onChange: pm.setMaster)
} trailing: { EmptyView() } } trailing: { EmptyView() }
Spacer(minLength: 12) Spacer(minLength: 16)
} }
.padding(.horizontal, 24) .padding(.horizontal, 24)
} }
.scrollIndicators(.hidden) .scrollIndicators(.hidden)
} }
} }
}
/// [Chip] in the sheet's own colours: selected is the label colour, so it reads on any tint. /// [Chip] in the sheet's own colours: selected is the label colour, so it reads on any tint.
private struct PresetChip: View { private struct PresetChip: View {
@@ -665,12 +669,13 @@ private struct StemFader: View {
let setting: StemSetting let setting: StemSetting
let level: Float let level: Float
let enabled: Bool let enabled: Bool
var height: CGFloat = 46
@Environment(\.palette) private var p @Environment(\.palette) private var p
private var pm: PlaybackManager { AppGraph.shared.playback } private var pm: PlaybackManager { AppGraph.shared.playback }
var body: some View { var body: some View {
let color = Color(hex: Stems.color(name)) let color = Color(hex: Stems.color(name))
FaderRow(dot: setting.muted ? p.label3 : color, label: Stems.label(name), percent: Int(stemTaper(setting.level) * 100), dim: setting.muted) { FaderRow(dot: setting.muted ? p.label3 : color, label: Stems.label(name), percent: Int(stemTaper(setting.level) * 100), dim: setting.muted, height: height) {
VStack(spacing: 0) { VStack(spacing: 0) {
FaderSlider(value: setting.level, fill: setting.muted ? p.label3 : color, detent: StemSetting.unity, enabled: enabled) { FaderSlider(value: setting.level, fill: setting.muted ? p.label3 : color, detent: StemSetting.unity, enabled: enabled) {
pm.setStemLevel(name, $0) pm.setStemLevel(name, $0)
@@ -702,6 +707,7 @@ private struct FaderRow<Fader: View, Trailing: View>: View {
let label: String let label: String
let percent: Int let percent: Int
let dim: Bool let dim: Bool
var height: CGFloat = 46
@ViewBuilder let fader: Fader @ViewBuilder let fader: Fader
@ViewBuilder let trailing: Trailing @ViewBuilder let trailing: Trailing
@Environment(\.palette) private var p @Environment(\.palette) private var p
@@ -718,7 +724,7 @@ private struct FaderRow<Fader: View, Trailing: View>: View {
.frame(width: 36, alignment: .trailing) .frame(width: 36, alignment: .trailing)
trailing trailing
} }
.frame(height: 46) .frame(height: height)
} }
} }
+2 -2
View File
@@ -6,8 +6,8 @@ options:
settings: settings:
base: base:
SWIFT_VERSION: "5.0" SWIFT_VERSION: "5.0"
MARKETING_VERSION: "1.0.13" MARKETING_VERSION: "1.0.16"
CURRENT_PROJECT_VERSION: "14" CURRENT_PROJECT_VERSION: "17"
targets: targets:
Stemify: Stemify:
type: application type: application
+18
View File
@@ -15,6 +15,24 @@
"tintColor": "#FA2D48", "tintColor": "#FA2D48",
"category": "entertainment", "category": "entertainment",
"versions": [ "versions": [
{
"version": "1.0.15",
"buildVersion": "16",
"date": "2026-10-20T14:22:36-07:00",
"localizedDescription": "Mixer faders use the spare height on Now Playing, with more room above the seek bar. Unsigned IPA for Sideloadly, AltStore, SideStore or Feather.",
"downloadURL": "https://git.iamchrisama.com/cdnutter/stemify/releases/download/ios-v1.0.15/stemify-1.0.15.ipa",
"size": 1626425,
"minOSVersion": "17.0"
},
{
"version": "1.0.14",
"buildVersion": "15",
"date": "2026-10-20T14:14:26-07:00",
"localizedDescription": "Reorderable tab bar (Songs, Albums, Artists, Genres, Playlists, Search, Downloads), AirPlay route picker in Now Playing, debug logging with log export in Settings, more room above the seeker. Unsigned IPA.",
"downloadURL": "https://git.iamchrisama.com/cdnutter/stemify/releases/download/ios-v1.0.14/stemify-1.0.14.ipa",
"size": 1625201,
"minOSVersion": "17.0"
},
{ {
"version": "1.0.13", "version": "1.0.13",
"buildVersion": "14", "buildVersion": "14",