Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8074b9b
Add native iOS listener app (v1 scaffold)
Aug 2, 2026
e9e8a20
Fix stall watchdog false-positives; add app icon and signing
Aug 2, 2026
7bab741
Fix silent playback: player never started, and iOS probe was unanswered
Aug 2, 2026
16683f5
Separate playing a station from browsing one
Aug 2, 2026
337c344
Add per-device toggles for artist info and track trivia
Aug 2, 2026
efd5a5d
Move track trivia behind an ⓘ badge on the cover art
Aug 2, 2026
f81f4e0
Move the artist bio behind a tap on the artist's name
Aug 2, 2026
6b01f5c
Add appearance setting, mini-player marquee, station skip on lock screen
Aug 2, 2026
ddb2b94
Add local favorite stations, pinned to the top of the list
Aug 2, 2026
ea6fd34
Add optional operator sign-in, unlocking the heart action
Aug 2, 2026
5b2f589
Open the tuned station from the mini player as a slide-up
Aug 2, 2026
7f538f1
Add a heart action to the station-row swipe for signed-in operators
Aug 2, 2026
060055b
Give the mini player's text column the full width it has
Aug 2, 2026
7338a00
Restructure the station screen's now-playing block
Aug 2, 2026
9b34a93
Give the now-playing block a visible container
Aug 2, 2026
d495fc6
Lay the now-playing card out as a row with a labelled action
Aug 2, 2026
e2dba28
Match the web player's accent, with a picker to override it
Aug 2, 2026
f97383b
Add an audio-output picker to the station card
Aug 2, 2026
4931c8a
Stop list rows colliding with the pinned section headers
Aug 2, 2026
862b965
Add Chromecast support
MMagTech Aug 2, 2026
430a789
Make the list headers scroll instead of pinning
MMagTech Aug 2, 2026
7c34fce
Actually start Cast discovery
MMagTech Aug 2, 2026
eed597b
Move the heart to the trailing edge, and offer it as a remote command
MMagTech Aug 2, 2026
08f5e1f
Keep the station card a fixed height across track changes
MMagTech Aug 2, 2026
c54420a
Scroll the station title instead of wrapping it
MMagTech Aug 2, 2026
c3d635e
Align the three station-card controls to each other
MMagTech Aug 2, 2026
67feb73
Add tuning static between station switches
MMagTech Aug 2, 2026
3b148cf
Add a full-screen display mode
MMagTech Aug 2, 2026
26c2270
Add station skip to display mode; give marquees their full width and …
MMagTech Aug 2, 2026
b2ef32c
Sit both artwork badges inside the cover on opposite corners
MMagTech Aug 2, 2026
06d0d29
Align the artwork badges to the cover, not to an invisible larger box
MMagTech Aug 2, 2026
eb83c23
Open display mode with a trailing swipe on a station row
MMagTech Aug 2, 2026
a871b02
Show skip and heart once display mode starts playing a station
MMagTech Aug 2, 2026
1fe465b
Tell the difference between no network and a struggling stream
MMagTech Aug 2, 2026
3abc081
Stop short titles inheriting the previous title's width and scrolling
MMagTech Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ venv/
alchemyfm-backup/
cookies.txt
login_out.txt

# Google Cast SDK — 141 MB, fetched by ios/Vendor/fetch-cast-sdk.sh
ios/Vendor/GoogleCast.xcframework/
331 changes: 331 additions & 0 deletions ios/AlchemyFM.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {

/* Begin PBXBuildFile section */
1A0000000000000000000012 /* GoogleCast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000011 /* GoogleCast.xcframework */; };
1A0000000000000000000014 /* GoogleCast.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1A0000000000000000000011 /* GoogleCast.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
1A0000000000000000000015 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
1A0000000000000000000014 /* GoogleCast.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
1A0000000000000000000004 /* AlchemyFM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AlchemyFM.app; sourceTree = BUILT_PRODUCTS_DIR; };
1A0000000000000000000011 /* GoogleCast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GoogleCast.xcframework; path = Vendor/GoogleCast.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
1A0000000000000000000010 /* Exceptions for "AlchemyFM" folder in "AlchemyFM" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 1A0000000000000000000006 /* AlchemyFM */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
1A0000000000000000000005 /* AlchemyFM */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
1A0000000000000000000010 /* Exceptions for "AlchemyFM" folder in "AlchemyFM" target */,
);
path = AlchemyFM;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
1A0000000000000000000008 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1A0000000000000000000012 /* GoogleCast.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
1A0000000000000000000002 = {
isa = PBXGroup;
children = (
1A0000000000000000000005 /* AlchemyFM */,
1A0000000000000000000013 /* Frameworks */,
1A0000000000000000000003 /* Products */,
);
sourceTree = "<group>";
};
1A0000000000000000000013 /* Frameworks */ = {
isa = PBXGroup;
children = (
1A0000000000000000000011 /* GoogleCast.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
1A0000000000000000000003 /* Products */ = {
isa = PBXGroup;
children = (
1A0000000000000000000004 /* AlchemyFM.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
1A0000000000000000000006 /* AlchemyFM */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1A000000000000000000000B /* Build configuration list for PBXNativeTarget "AlchemyFM" */;
buildPhases = (
1A0000000000000000000007 /* Sources */,
1A0000000000000000000008 /* Frameworks */,
1A0000000000000000000009 /* Resources */,
1A0000000000000000000015 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
1A0000000000000000000005 /* AlchemyFM */,
);
name = AlchemyFM;
packageProductDependencies = (
);
productName = AlchemyFM;
productReference = 1A0000000000000000000004 /* AlchemyFM.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
1A0000000000000000000001 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2660;
LastUpgradeCheck = 2660;
TargetAttributes = {
1A0000000000000000000006 = {
CreatedOnToolsVersion = 26.6;
};
};
};
buildConfigurationList = 1A000000000000000000000A /* Build configuration list for PBXProject "AlchemyFM" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 1A0000000000000000000002;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 1A0000000000000000000003 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
1A0000000000000000000006 /* AlchemyFM */,
);
};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
1A0000000000000000000009 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
1A0000000000000000000007 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */

/* Begin XCBuildConfiguration section */
1A000000000000000000000C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
1A000000000000000000000D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
1A000000000000000000000E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = ZMUB88RZ5D;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = AlchemyFM/Info.plist;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Vendor",
);
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = fm.alchemy.AlchemyFM;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
1A000000000000000000000F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = ZMUB88RZ5D;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = AlchemyFM/Info.plist;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Vendor",
);
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = fm.alchemy.AlchemyFM;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
1A000000000000000000000A /* Build configuration list for PBXProject "AlchemyFM" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1A000000000000000000000C /* Debug */,
1A000000000000000000000D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1A000000000000000000000B /* Build configuration list for PBXNativeTarget "AlchemyFM" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1A000000000000000000000E /* Debug */,
1A000000000000000000000F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 1A0000000000000000000001 /* Project object */;
}
Loading
Loading