Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75c6d89608 | ||
|
|
f065c95422 | ||
|
|
c17d6f174b | ||
|
|
e7e4fcc9d6 |
No files matched your search
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in new issue
Block a user