From bb785beff28bd7c8f1809e9e138e14ed45e06f68 Mon Sep 17 00:00:00 2001 From: Marty Ulrich Date: Tue, 24 Sep 2024 19:26:32 -0700 Subject: [PATCH 1/9] Mobile 220 fix filter for songs under 30 seconds (#315) * fix build * fix ios build * change google sign in to idToken * update recaptcha * fix duration filter --- .../AudioPlayer/PlayQueue/PlayQueue.swift | 4 + .../VLCAudioPlayer/VLCAudioPlayer.swift | 8 +- .../Mocks/Use Cases/MockSignUpUseCase.swift | 4 - .../MockWalletNFTTracksUseCase.swift | 4 + .../Extensions/NFTTrack+FilterAndSort.swift | 38 +++++ .../Screens/Library/UI/LibraryViewModel.swift | 9 +- .../UI/ViewModels/LandingViewModel.swift | 4 +- .../AudioPlayerTests/PlayQueueTests.swift | 16 +- iosApp/Tests/LibraryTests/Library.xctestplan | 25 +++ .../LibraryTests/LibraryFilterTests.swift | 41 +++++ iosApp/iosApp.xcodeproj/project.pbxproj | 143 +++++++++++++++++- .../public/models/mocks/NFTTrackMocks.kt | 20 +-- 12 files changed, 282 insertions(+), 34 deletions(-) create mode 100644 iosApp/Modules/Screens/Library/Extensions/NFTTrack+FilterAndSort.swift create mode 100644 iosApp/Tests/LibraryTests/Library.xctestplan create mode 100644 iosApp/Tests/LibraryTests/LibraryFilterTests.swift diff --git a/iosApp/Modules/AudioPlayer/PlayQueue/PlayQueue.swift b/iosApp/Modules/AudioPlayer/PlayQueue/PlayQueue.swift index 57ca66e4..9f2c15ff 100644 --- a/iosApp/Modules/AudioPlayer/PlayQueue/PlayQueue.swift +++ b/iosApp/Modules/AudioPlayer/PlayQueue/PlayQueue.swift @@ -42,6 +42,10 @@ struct PlayQueue { get { filters.duration } set { guard newValue != durationFilter else { return } + guard let newValue, newValue > 0 else { + filters.duration = nil + return + } filters.duration = newValue } } diff --git a/iosApp/Modules/AudioPlayer/VLCAudioPlayer/VLCAudioPlayer.swift b/iosApp/Modules/AudioPlayer/VLCAudioPlayer/VLCAudioPlayer.swift index d2bb62c0..4dfb58ca 100644 --- a/iosApp/Modules/AudioPlayer/VLCAudioPlayer/VLCAudioPlayer.swift +++ b/iosApp/Modules/AudioPlayer/VLCAudioPlayer/VLCAudioPlayer.swift @@ -9,6 +9,7 @@ import ModuleLinker import Utilities import OrderedCollections +@MainActor public class VLCAudioPlayer: ObservableObject { public enum PlaybackState { case playing @@ -24,7 +25,6 @@ public class VLCAudioPlayer: ObservableObject { lazy private var delegate: VLCAudioPlayerDelegate = VLCAudioPlayerDelegate() private var cancels = Set() - @MainActor @Published public private(set) var loadingProgress: [NFTTrack: Double] = [:] private var _errors = PassthroughSubject() @@ -149,7 +149,6 @@ public class VLCAudioPlayer: ObservableObject { } } - @MainActor public func downloadTrack(_ track: NFTTrack) async throws { guard trackIsDownloaded(track) == false, loadingProgress[track] == nil else { return @@ -168,7 +167,6 @@ public class VLCAudioPlayer: ObservableObject { } } - @MainActor public func cancelDownload(_ track: NFTTrack) { fileManager.cancelDownload(track: track) } @@ -249,6 +247,8 @@ public class VLCAudioPlayer: ObservableObject { } deinit { - stop() + Task { [weak self] in + await self?.stop() + } } } diff --git a/iosApp/Modules/Mocks/Use Cases/MockSignUpUseCase.swift b/iosApp/Modules/Mocks/Use Cases/MockSignUpUseCase.swift index 8a308f4b..0f1853a1 100644 --- a/iosApp/Modules/Mocks/Use Cases/MockSignUpUseCase.swift +++ b/iosApp/Modules/Mocks/Use Cases/MockSignUpUseCase.swift @@ -10,10 +10,6 @@ public class MockSignupUseCase: SignupUseCase { } - public func registerUser(email: String, password: String, passwordConfirmation: String, verificationCode: String, humanVerificationCode: String) async throws { - - } - public func requestEmailConfirmationCode(email: String, humanVerificationCode: String, mustExists: Bool) async throws { } diff --git a/iosApp/Modules/Mocks/Use Cases/MockWalletNFTTracksUseCase.swift b/iosApp/Modules/Mocks/Use Cases/MockWalletNFTTracksUseCase.swift index 31012bef..884dafe2 100644 --- a/iosApp/Modules/Mocks/Use Cases/MockWalletNFTTracksUseCase.swift +++ b/iosApp/Modules/Mocks/Use Cases/MockWalletNFTTracksUseCase.swift @@ -3,6 +3,10 @@ import shared class MockWalletNFTTracksUseCase: WalletNFTTracksUseCase { var walletSynced: any Kotlinx_coroutines_coreFlow { fatalError() } + + func getNFTTrack(id: String) -> NFTTrack? { + nil + } func getAllTracks() async throws -> [NFTTrack] { [] diff --git a/iosApp/Modules/Screens/Library/Extensions/NFTTrack+FilterAndSort.swift b/iosApp/Modules/Screens/Library/Extensions/NFTTrack+FilterAndSort.swift new file mode 100644 index 00000000..95b554af --- /dev/null +++ b/iosApp/Modules/Screens/Library/Extensions/NFTTrack+FilterAndSort.swift @@ -0,0 +1,38 @@ +import Foundation +import shared +import AudioPlayer + +extension NFTTrack { + func isAboveDurationFilter(_ durationFilter: Int?) -> Bool { + let trackIsAboveDurationFilter: Bool + if let durationFilter { + if duration > durationFilter { + trackIsAboveDurationFilter = true + } else { + trackIsAboveDurationFilter = false + } + } else { + trackIsAboveDurationFilter = true + } + return trackIsAboveDurationFilter + } + + func containsSearchText(_ searchText: String?) -> Bool { + guard let searchText, searchText.isEmpty == false else { + return true + } + + return title.localizedCaseInsensitiveContains(searchText) || + artists.contains { $0.localizedCaseInsensitiveContains(searchText) } + } +} + +extension [NFTTrack] { + func filteredAndSorted(sort: Sort, searchText: String?, durationFilter: Int?) -> [NFTTrack] { + filter { track in + return track.isAboveDurationFilter(durationFilter) && + track.containsSearchText(searchText) + } + .sorted(by: sort.comparator) + } +} diff --git a/iosApp/Modules/Screens/Library/UI/LibraryViewModel.swift b/iosApp/Modules/Screens/Library/UI/LibraryViewModel.swift index db0e1407..1bed605d 100644 --- a/iosApp/Modules/Screens/Library/UI/LibraryViewModel.swift +++ b/iosApp/Modules/Screens/Library/UI/LibraryViewModel.swift @@ -34,9 +34,8 @@ class LibraryViewModel: ObservableObject { private func sortAndFilterTracks() { filteredSortedTracks = tracks .filter { track in - return (durationFilter.flatMap { track.duration > $0 } ?? true) && - (searchText.isEmpty == false ? - (track.title.localizedCaseInsensitiveContains(searchText) || track.artists.first { $0.localizedCaseInsensitiveContains(searchText) } != nil) : true) + return track.isAboveDurationFilter(durationFilter) && + track.containsSearchText(searchText) } .sorted(by: sort.comparator) } @@ -91,7 +90,7 @@ class LibraryViewModel: ObservableObject { errors.append(error) } } - + Task { [weak self] in self?.walletIsConnected = try await self?.hasWalletConnectionsUseCase.hasWalletConnections().boolValue == true await self?.refresh() @@ -243,7 +242,7 @@ class LibraryViewModel: ObservableObject { func toggleLengthFilter() { if durationFilter == 30 { - durationFilter = 0 + durationFilter = nil } else { durationFilter = 30 } diff --git a/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel.swift b/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel.swift index 56ededd2..f2c215c1 100644 --- a/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel.swift +++ b/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel.swift @@ -32,7 +32,7 @@ class LandingViewModel: ObservableObject { Task { [weak self] in guard let self else { return } do { - recaptcha = try await Recaptcha.getClient(withSiteKey: EnvironmentVariable.recaptchaKey.value) + recaptcha = try await Recaptcha.fetchClient(withSiteKey: EnvironmentVariable.recaptchaKey.value) } catch let error as RecaptchaError { errorLogger.logError("RecaptchaClient creation error: \(String(describing: error.errorMessage)).") } catch { @@ -152,7 +152,7 @@ class LandingViewModel: ObservableObject { } func handleGoogleSignIn(result: GIDSignInResult?, error: Error?) { - guard let idToken = result?.user.accessToken.tokenString else { + guard let idToken = result?.user.idToken?.tokenString else { //TODO: localize handleError("Failed to sign in with Google"); return } diff --git a/iosApp/Tests/AudioPlayerTests/PlayQueueTests.swift b/iosApp/Tests/AudioPlayerTests/PlayQueueTests.swift index 43cdc941..aab36b40 100644 --- a/iosApp/Tests/AudioPlayerTests/PlayQueueTests.swift +++ b/iosApp/Tests/AudioPlayerTests/PlayQueueTests.swift @@ -20,13 +20,13 @@ final class PlayQueueTests: XCTestCase { UserDefaults.standard.removePersistentDomain(forName: bundleID) } let tracks = [ - NFTTrack(id: "0", policyId: "B", title: "i", assetName: "Asset2", amount: 1, imageUrl: "", audioUrl: "", duration: 200, artists: ["Artist 2"], genres: ["Rock"], moods: ["Sad"], isDownloaded: true), - NFTTrack(id: "1", policyId: "A", title: "f", assetName: "Asset1", amount: 1, imageUrl: "", audioUrl: "", duration: 400, artists: ["Artist 1"], genres: ["Pop"], moods: ["Happy"], isDownloaded: true), - NFTTrack(id: "2", policyId: "C", title: "h", assetName: "Asset3", amount: 1, imageUrl: "", audioUrl: "", duration: 300, artists: ["Artist 3"], genres: ["Jazz"], moods: ["Relaxed"], isDownloaded: true), - NFTTrack(id: "3", policyId: "r", title: "g", assetName: "Asset4", amount: 1, imageUrl: "", audioUrl: "", duration: 500, artists: ["Artist 4"], genres: ["Jazz"], moods: ["Relaxed"], isDownloaded: true), - NFTTrack(id: "4", policyId: "z", title: "e", assetName: "Asset7", amount: 1, imageUrl: "", audioUrl: "", duration: 44, artists: ["Artist 7"], genres: ["Jazz"], moods: ["Relaxed"], isDownloaded: true), - NFTTrack(id: "5", policyId: "e", title: "z", assetName: "Asset5", amount: 1, imageUrl: "", audioUrl: "", duration: 30, artists: ["Artist 5"], genres: ["Jazz"], moods: ["Relaxed"], isDownloaded: true), - NFTTrack(id: "6", policyId: "d", title: "r", assetName: "Asset6", amount: 1, imageUrl: "", audioUrl: "", duration: 44, artists: ["Artist 6"], genres: ["Jazz"], moods: ["Relaxed"], isDownloaded: true), + NFTTrack(id: "0", policyId: "B", title: "i", assetName: "Asset2", amount: 1, imageUrl: "", audioUrl: "", duration: 200, artists: ["Artist 2"], genres: ["Rock"], moods: ["Sad"], isStreamToken: false, isDownloaded: true), + NFTTrack(id: "1", policyId: "A", title: "f", assetName: "Asset1", amount: 1, imageUrl: "", audioUrl: "", duration: 400, artists: ["Artist 1"], genres: ["Pop"], moods: ["Happy"], isStreamToken: false, isDownloaded: true), + NFTTrack(id: "2", policyId: "C", title: "h", assetName: "Asset3", amount: 1, imageUrl: "", audioUrl: "", duration: 300, artists: ["Artist 3"], genres: ["Jazz"], moods: ["Relaxed"], isStreamToken: false, isDownloaded: true), + NFTTrack(id: "3", policyId: "r", title: "g", assetName: "Asset4", amount: 1, imageUrl: "", audioUrl: "", duration: 500, artists: ["Artist 4"], genres: ["Jazz"], moods: ["Relaxed"], isStreamToken: false, isDownloaded: true), + NFTTrack(id: "4", policyId: "z", title: "e", assetName: "Asset7", amount: 1, imageUrl: "", audioUrl: "", duration: 44, artists: ["Artist 7"], genres: ["Jazz"], moods: ["Relaxed"], isStreamToken: false, isDownloaded: true), + NFTTrack(id: "5", policyId: "e", title: "z", assetName: "Asset5", amount: 1, imageUrl: "", audioUrl: "", duration: 30, artists: ["Artist 5"], genres: ["Jazz"], moods: ["Relaxed"], isStreamToken: false, isDownloaded: true), + NFTTrack(id: "6", policyId: "d", title: "r", assetName: "Asset6", amount: 1, imageUrl: "", audioUrl: "", duration: 44, artists: ["Artist 6"], genres: ["Jazz"], moods: ["Relaxed"], isStreamToken: false, isDownloaded: true), ] var playQueue = PlayQueue() playQueue.originalTracks = Set(tracks) @@ -240,7 +240,7 @@ final class PlayQueueTests: XCTestCase { func testSeekToTrack() throws { try playQueue.seekToTrack(tracks[4]) XCTAssertEqual(try playQueue.currentTrack(), tracks[4]) - XCTAssertThrowsError(try playQueue.seekToTrack(NFTTrack(id: "999", policyId: "999", title: "hi", assetName: "hi", amount: 4, imageUrl: "", audioUrl: "", duration: 9, artists: [], genres: [], moods: [], isDownloaded: false))) + XCTAssertThrowsError(try playQueue.seekToTrack(NFTTrack(id: "999", policyId: "999", title: "hi", assetName: "hi", amount: 4, imageUrl: "", audioUrl: "", duration: 9, artists: [], genres: [], moods: [], isStreamToken: false, isDownloaded: false))) } func testCycleRepeatMode() { diff --git a/iosApp/Tests/LibraryTests/Library.xctestplan b/iosApp/Tests/LibraryTests/Library.xctestplan new file mode 100644 index 00000000..fa80f2af --- /dev/null +++ b/iosApp/Tests/LibraryTests/Library.xctestplan @@ -0,0 +1,25 @@ +{ + "configurations" : [ + { + "id" : "235697C8-B3B7-45DC-A2A3-2A64B55CB429", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:iosApp.xcodeproj", + "identifier" : "7651619D2C8E368C00E79860", + "name" : "LibraryTests" + } + } + ], + "version" : 1 +} diff --git a/iosApp/Tests/LibraryTests/LibraryFilterTests.swift b/iosApp/Tests/LibraryTests/LibraryFilterTests.swift new file mode 100644 index 00000000..d10cff62 --- /dev/null +++ b/iosApp/Tests/LibraryTests/LibraryFilterTests.swift @@ -0,0 +1,41 @@ +import XCTest + +@testable import Library +import shared + +final class LibraryFilterTests: XCTestCase { + func testDurationFilter() { + XCTAssertTrue( + NFTTrack(id: "1", policyId: "1", title: "", assetName: "", amount: 0, imageUrl: "", audioUrl: "", duration: 40, artists: [], genres: [], moods: [], isStreamToken: false, isDownloaded: false) + .isAboveDurationFilter(30) + ) + + XCTAssertFalse( + NFTTrack(id: "1", policyId: "1", title: "", assetName: "", amount: 0, imageUrl: "", audioUrl: "", duration: 40, artists: [], genres: [], moods: [], isStreamToken: false, isDownloaded: false) + .isAboveDurationFilter(50) + ) + + XCTAssertFalse( + NFTTrack(id: "1", policyId: "1", title: "", assetName: "", amount: 0, imageUrl: "", audioUrl: "", duration: 30, artists: [], genres: [], moods: [], isStreamToken: false, isDownloaded: false) + .isAboveDurationFilter(30) + ) + + XCTAssertFalse( + NFTTrack(id: "1", policyId: "1", title: "", assetName: "", amount: 0, imageUrl: "", audioUrl: "", duration: -1, artists: [], genres: [], moods: [], isStreamToken: false, isDownloaded: false) + .isAboveDurationFilter(0) + ) + } + + func testContainsSearchText() { + let track = NFTTrack(id: "1", policyId: "1", title: "title", assetName: "", amount: 0, imageUrl: "", audioUrl: "", duration: 30, artists: ["Test"], genres: [], moods: [], isStreamToken: false, isDownloaded: false) + XCTAssertTrue(track.containsSearchText("T")) + XCTAssertTrue(track.containsSearchText("Te")) + XCTAssertTrue(track.containsSearchText("Test")) + XCTAssertTrue(track.containsSearchText("s")) + XCTAssertTrue(track.containsSearchText("es")) + XCTAssertTrue(track.containsSearchText("ti")) + XCTAssertTrue(track.containsSearchText("l")) + XCTAssertTrue(track.containsSearchText("t")) + XCTAssertTrue(track.containsSearchText("")) + } +} diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 29cbf1f8..c4388a2d 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -141,6 +141,9 @@ 7650E1702B17CB0A00D686B8 /* AudioPlayer.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 76C76132291F445B0054D2F3 /* AudioPlayer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7650E1742B17CB2A00D686B8 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 7650E1732B17CB2A00D686B8 /* Kingfisher */; }; 7650E1762B17CB3600D686B8 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; productRef = 7650E1752B17CB3600D686B8 /* QRCodeReader */; }; + 765161982C8E35D500E79860 /* NFTTrack+FilterAndSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 765161972C8E35D500E79860 /* NFTTrack+FilterAndSort.swift */; }; + 765161A12C8E368C00E79860 /* LibraryFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 765161A02C8E368C00E79860 /* LibraryFilterTests.swift */; }; + 765161A22C8E368C00E79860 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76562514284BEF30004B6E4B /* Library.framework */; platformFilter = ios; }; 7651D6D427DFA33100FF41B3 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7651D6D327DFA33100FF41B3 /* iOSApp.swift */; }; 7656251F284BEF68004B6E4B /* ModuleLinker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76D01D9E28164B2100305605 /* ModuleLinker.framework */; }; 76562525284BEF6C004B6E4B /* Resolver in Frameworks */ = {isa = PBXBuildFile; productRef = 76562524284BEF6C004B6E4B /* Resolver */; }; @@ -636,6 +639,13 @@ remoteGlobalIDString = 76C76131291F445B0054D2F3; remoteInfo = AudioPlayer; }; + 765161A32C8E368C00E79860 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7555FF73242A565900829871 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 76562513284BEF30004B6E4B; + remoteInfo = Library; + }; 76562521284BEF68004B6E4B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7555FF73242A565900829871 /* Project object */; @@ -1037,6 +1047,10 @@ 7650E1442B17A0F300D686B8 /* XPubScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPubScannerView.swift; sourceTree = ""; }; 7650E14B2B17B3FA00D686B8 /* Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7650E1602B17B51300D686B8 /* ProfileModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModule.swift; sourceTree = ""; }; + 765161972C8E35D500E79860 /* NFTTrack+FilterAndSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NFTTrack+FilterAndSort.swift"; sourceTree = ""; }; + 7651619E2C8E368C00E79860 /* LibraryTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LibraryTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 765161A02C8E368C00E79860 /* LibraryFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterTests.swift; sourceTree = ""; }; + 765161A82C8E39E900E79860 /* Library.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Library.xctestplan; path = Tests/LibraryTests/Library.xctestplan; sourceTree = ""; }; 7651D6D327DFA33100FF41B3 /* iOSApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; 76562514284BEF30004B6E4B /* Library.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Library.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 76562526284BEFB7004B6E4B /* Library.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library.swift; sourceTree = ""; }; @@ -1396,6 +1410,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7651619B2C8E368C00E79860 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 765161A22C8E368C00E79860 /* Library.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 76562511284BEF30004B6E4B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1512,6 +1534,7 @@ 7555FF72242A565900829871 = { isa = PBXGroup; children = ( + 765161A82C8E39E900E79860 /* Library.xctestplan */, 76D99EC62B1934AD002FB4B2 /* Kotlin */, 761ADD3A2AE734AA001B56CF /* shared.xcframework */, 76EAFC5C2A0AE33E00ACD862 /* Secrets.xcconfig */, @@ -1551,6 +1574,7 @@ 768C69AC2B5CD971001A1B1A /* Mocks.framework */, 76FFD1192B9554DF00D77309 /* AudioPlayerTests.xctest */, 76F8E9022BA5AC5D0013595D /* NowPlayingApp.app */, + 7651619E2C8E368C00E79860 /* LibraryTests.xctest */, ); name = Products; sourceTree = ""; @@ -1885,6 +1909,7 @@ children = ( 764838D62B0C8DFD00D6905B /* APITests */, 76FFD11A2B9554DF00D77309 /* AudioPlayerTests */, + 7651619F2C8E368C00E79860 /* LibraryTests */, 768C697F2B5CC3CF001A1B1A /* ProfileTests */, 76452A852B5A3AB90001F47E /* UtilitiesTests */, ); @@ -1971,6 +1996,22 @@ path = Profile; sourceTree = ""; }; + 765161962C8E35B800E79860 /* Extensions */ = { + isa = PBXGroup; + children = ( + 765161972C8E35D500E79860 /* NFTTrack+FilterAndSort.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 7651619F2C8E368C00E79860 /* LibraryTests */ = { + isa = PBXGroup; + children = ( + 765161A02C8E368C00E79860 /* LibraryFilterTests.swift */, + ); + path = LibraryTests; + sourceTree = ""; + }; 7651D6D527DFA35100FF41B3 /* App */ = { isa = PBXGroup; children = ( @@ -2182,6 +2223,7 @@ isa = PBXGroup; children = ( 768B498D2903B89F0099911D /* LibraryModule.swift */, + 765161962C8E35B800E79860 /* Extensions */, 7625D14F2942B0740033C669 /* Navigation */, 768B49892903B89F0099911D /* Resources */, 768B49812903B89F0099911D /* UI */, @@ -3021,6 +3063,24 @@ productReference = 7650E14B2B17B3FA00D686B8 /* Profile.framework */; productType = "com.apple.product-type.framework"; }; + 7651619D2C8E368C00E79860 /* LibraryTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 765161A52C8E368C00E79860 /* Build configuration list for PBXNativeTarget "LibraryTests" */; + buildPhases = ( + 7651619A2C8E368C00E79860 /* Sources */, + 7651619B2C8E368C00E79860 /* Frameworks */, + 7651619C2C8E368C00E79860 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 765161A42C8E368C00E79860 /* PBXTargetDependency */, + ); + name = LibraryTests; + productName = LibraryTests; + productReference = 7651619E2C8E368C00E79860 /* LibraryTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 76562513284BEF30004B6E4B /* Library */ = { isa = PBXNativeTarget; buildConfigurationList = 7656251C284BEF30004B6E4B /* Build configuration list for PBXNativeTarget "Library" */; @@ -3225,7 +3285,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1530; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1520; ORGANIZATIONNAME = NEWM; TargetAttributes = { @@ -3275,6 +3335,9 @@ CreatedOnToolsVersion = 15.0.1; LastSwiftMigration = 1500; }; + 7651619D2C8E368C00E79860 = { + CreatedOnToolsVersion = 15.4; + }; 76562513284BEF30004B6E4B = { CreatedOnToolsVersion = 13.4.1; LastSwiftMigration = 1340; @@ -3349,6 +3412,7 @@ 76452A832B5A3AB90001F47E /* UtilitiesTests */, 76FFD1182B9554DF00D77309 /* AudioPlayerTests */, 76F8E9012BA5AC5D0013595D /* NowPlayingApp */, + 7651619D2C8E368C00E79860 /* LibraryTests */, ); }; /* End PBXProject section */ @@ -3473,6 +3537,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7651619C2C8E368C00E79860 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 76562512284BEF30004B6E4B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3781,10 +3852,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7651619A2C8E368C00E79860 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 765161A12C8E368C00E79860 /* LibraryFilterTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 76562510284BEF30004B6E4B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 765161982C8E35D500E79860 /* NFTTrack+FilterAndSort.swift in Sources */, 768B49912903B89F0099911D /* LibraryViewModel.swift in Sources */, 768B49932903B89F0099911D /* LibraryViewActionHandling.swift in Sources */, 768B49972903B89F0099911D /* Strings.swift in Sources */, @@ -4119,6 +4199,12 @@ target = 76C76131291F445B0054D2F3 /* AudioPlayer */; targetProxy = 7650E1712B17CB0A00D686B8 /* PBXContainerItemProxy */; }; + 765161A42C8E368C00E79860 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 76562513284BEF30004B6E4B /* Library */; + targetProxy = 765161A32C8E368C00E79860 /* PBXContainerItemProxy */; + }; 76562522284BEF68004B6E4B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 76D01D9D28164B2100305605 /* ModuleLinker */; @@ -5495,6 +5581,52 @@ }; name = Release; }; + 765161A62C8E368C00E79860 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = TM6M46M9D2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.newm.ios.LibraryTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 765161A72C8E368C00E79860 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = TM6M46M9D2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.newm.ios.LibraryTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 7656251D284BEF30004B6E4B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -6210,6 +6342,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 765161A52C8E368C00E79860 /* Build configuration list for PBXNativeTarget "LibraryTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 765161A62C8E368C00E79860 /* Debug */, + 765161A72C8E368C00E79860 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 7656251C284BEF30004B6E4B /* Build configuration list for PBXNativeTarget "Library" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/shared/src/commonMain/kotlin/io.newm.shared/public/models/mocks/NFTTrackMocks.kt b/shared/src/commonMain/kotlin/io.newm.shared/public/models/mocks/NFTTrackMocks.kt index f2b7d041..3e30abfd 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/public/models/mocks/NFTTrackMocks.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/public/models/mocks/NFTTrackMocks.kt @@ -9,7 +9,7 @@ val mockTracks: List = listOf( name = "Dripdropz", imageUrl = "https://arweave.net/zBVmedCDTGBH06tTEA5u0aYFWkXPkr9w1GxGefxJIms", songUrl = "https://arweave.net/tWDP3Lr4U3vMy_iYwm-FPBa6ad0aaMNdwHa7MUAFjuo", - duration = 30, + duration = 20, artists = listOf("Esco") ), makeMockTrack( @@ -17,7 +17,7 @@ val mockTracks: List = listOf( name = "Lost In My Own Zone", imageUrl = "https://arweave.net/NkVFjDc24JfU4kgKE_vgz6hnuBpx_5ics2RTrrQEP6U", songUrl = "https://arweave.net/VkFPK-T7xkMgblDbxjfKjI7UXeyu23AOyg7pm4VjxzY", - duration = 30, + duration = 2, artists = listOf("Abyss") ), makeMockTrack( @@ -25,7 +25,7 @@ val mockTracks: List = listOf( name = "Sexiest Man Alive", imageUrl = "https://arweave.net/eYQ-pcX9qwh3KuVcdbClQQMkdgehula0ZhtSGomzdbg", songUrl = "https://arweave.net/KN9xqm4VroNdlgo30lA5hvkOPasgFIbSUqAVAPn8ob0", - duration = 30, + duration = -15, artists = listOf("Mike Lerman") ), makeMockTrack( @@ -33,7 +33,7 @@ val mockTracks: List = listOf( name = "Daisuke", imageUrl = "https://arweave.net/GlMlqHIPjwUtlPUfQxDdX1jWSjlKK1BCTBIekXgA66A", songUrl = "https://arweave.net/QpgjmWmAHNeRVgx_Ylwvh16i3aWd8BBgyq7f16gaUu0", - duration = 30, + duration = 45, artists = listOf("Danketsu", "Mirai Music", "NSTASIA") ), makeMockTrack( @@ -41,7 +41,7 @@ val mockTracks: List = listOf( name = "Love In The Water", imageUrl = "https://arweave.net/f5W8RZmAQimuz_vytFY9ofIzd9QpGaDIv2UXrrahTu4", songUrl = "https://arweave.net/DeVRF-RTkRRHoP4M-L9AjIu35ilzgclhLOrgQB2Px34", - duration = 30, + duration = 1, artists = listOf("NIDO") ), makeMockTrack( @@ -49,7 +49,7 @@ val mockTracks: List = listOf( name = "New Song", imageUrl = "https://arweave.net/xUauTN89ulvWAQ2Euz12ogF3EbDaiNPQKNe0I0Ib-mA", songUrl = "https://arweave.net/jeVMmGLmrtV3Dn-TfPkdCAn-Qjei4A2kFUOhFuvSCKU", - duration = 30, + duration = 50, artists = listOf("Esco") ), makeMockTrack( @@ -57,7 +57,7 @@ val mockTracks: List = listOf( name = "Bigger Dreams", imageUrl = "https://arweave.net/CuPFY2Ln7yUUhJX09G530kdPf93eGhAVlhjrtR7Jh5w", songUrl = "https://arweave.net/P141o0RDAjSYlVQgTDgHNAORQTkMYIVCprmD_dKMVss", - duration = 30, + duration = 93, artists = listOf("MURS") ), makeMockTrack( @@ -65,7 +65,7 @@ val mockTracks: List = listOf( name = "Space Cowboy", imageUrl = "https://arweave.net/qog8drrF9Oa55eWclrUejI65rn29gdcDX-Bj31VwBMc", songUrl = "https://arweave.net/W-PMgNX28f1RE1qwLG7SIU14-NPEmatFr51-zUAsqFI", - duration = 30, + duration = 123, artists = listOf("JUSE") ), makeMockTrack( @@ -73,7 +73,7 @@ val mockTracks: List = listOf( name = "Best Song Ever", imageUrl = "https://arweave.net/tlsj0fyL0BXU871-my1CvnNSBjMgXRn4zaO3taFVz3k", songUrl = "https://arweave.net/6HwXcWvOwfOWTljGYTlce0_zJjg1UslgkvNue6pau0E", - duration = 30, + duration = 324, artists = listOf("Esco") ), makeMockTrack( @@ -81,7 +81,7 @@ val mockTracks: List = listOf( name = "Underdog, Pt. 2", imageUrl = "https://arweave.net/eYQ-pcX9qwh3KuVcdbClQQMkdgehula0ZhtSGomzdbg", songUrl = "https://arweave.net/Em9XiS87I9ff8Wx2WOt2GIZ650gaiRTSjkNfJdvluLs", - duration = 30, + duration = 5, artists = listOf("Mike Lerman") ), makeMockTrack( From 03c539493a32c8b265e2790f7846b19c088ffcb9 Mon Sep 17 00:00:00 2001 From: Gohan Date: Wed, 25 Sep 2024 14:48:16 -0500 Subject: [PATCH 2/9] Share button implementation (#316) --- .../resources/src/main/res/values/strings.xml | 16 ++++++ .../feature/musicplayer/MusicPlayerViewer.kt | 20 +++----- .../share/RandomPhraseGenerator.kt | 25 ++++++++++ .../feature/musicplayer/share/ShareButton.kt | 49 +++++++++++++++++++ 4 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/RandomPhraseGenerator.kt create mode 100644 android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/ShareButton.kt diff --git a/android/core/resources/src/main/res/values/strings.xml b/android/core/resources/src/main/res/values/strings.xml index 195eef51..7b081afc 100644 --- a/android/core/resources/src/main/res/values/strings.xml +++ b/android/core/resources/src/main/res/values/strings.xml @@ -79,6 +79,22 @@ Your new password Search Second Fragment + 👉 Tap in and discover what you\'re missing: https://newm.io + 🚀 Vibing to %1$s by %2$s on the Newm App! 🎧 Don\'t miss out! + 🎶 Can\'t stop grooving to %1$s by %2$s. Catch the beat on Newm! 🔥 + 🎧 Jammin\' to %1$s by %2$s on repeat! Newm\'s got the tunes! 🚀 + 🎉 Just found my new anthem: %1$s by %2$s! Discover yours on Newm! 🎶 + 🔥 Turn it up! %1$s by %2$s is a whole vibe on Newm App! 🎶 + 🌟 If you\'re not listening to %1$s by %2$s on Newm, you\'re missing out! 🎧 + 🎶 Locked into the rhythm of %1$s by %2$s on Newm App. Check it out! 🔥 + 🚀 Fueling my day with %1$s by %2$s. Newm App has all the jams! 🎧 + 🎉 Newm App’s playlist on fire with %1$s by %2$s. Ready to vibe? 🔥 + 🎧 Tuning into the best beats with %1$s by %2$s on Newm! 🚀 + 🔥 Crank up %1$s by %2$s on Newm App. The track is lit! 🎶 + 🎉 Found my groove with %1$s by %2$s. Discover fresh beats on Newm! 🚀 + 🎧 You NEED to hear %1$s by %2$s! Now playing on Newm App! 🔥 + 🎶 Feeling the rhythm of %1$s by %2$s. Newm App = non-stop vibes! 🚀 + 🔥 Plug into the sound of %1$s by %2$s on Newm App. Your new favorite jam awaits! 🎧 Stars Earnings Followers this week diff --git a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt index 7d50ac72..caae5d18 100644 --- a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt +++ b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt @@ -1,5 +1,7 @@ package io.newm.feature.musicplayer +import android.content.Context +import android.content.Intent import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import androidx.compose.animation.animateColorAsState @@ -61,6 +63,8 @@ import io.newm.feature.musicplayer.models.PlaybackRepeatMode import io.newm.feature.musicplayer.models.PlaybackState import io.newm.feature.musicplayer.models.PlaybackStatus import io.newm.feature.musicplayer.models.Track +import io.newm.feature.musicplayer.share.ShareButton +import io.newm.feature.musicplayer.share.getRandomSharePhrase import io.newm.feature.musicplayer.viewmodel.PlaybackUiEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -254,8 +258,10 @@ fun PlaybackControlPanel( modifier = Modifier.padding(horizontal = 12.dp), onClick = { onEvent(PlaybackUiEvent.Next) }) Spacer(modifier = Modifier.weight(1f)) - // TODO: Implement share functionality - //ShareButton(onClick = {}) + ShareButton( + songTitle = playbackStatus.track?.title, + songArtist = playbackStatus.track?.artist + ) } } } @@ -338,16 +344,6 @@ fun NextTrackButton(onClick: () -> Unit, modifier: Modifier = Modifier) { } } -@Composable -fun ShareButton(onClick: () -> Unit, modifier: Modifier = Modifier) { - IconButton(modifier = modifier, onClick = onClick) { - Icon( - painter = painterResource(id = R.drawable.ic_share), - contentDescription = "Share Song", - tint = Color.White - ) - } -} @Composable fun RepeatButton( diff --git a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/RandomPhraseGenerator.kt b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/RandomPhraseGenerator.kt new file mode 100644 index 00000000..4714a927 --- /dev/null +++ b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/RandomPhraseGenerator.kt @@ -0,0 +1,25 @@ +package io.newm.feature.musicplayer.share + +import android.content.Context +import io.newm.core.resources.R + +fun Context.getRandomSharePhrase(songTitle: String, songArtist: String): String { + val phraseIds = listOf( + R.string.share_phrase_1, + R.string.share_phrase_2, + R.string.share_phrase_3, + R.string.share_phrase_4, + R.string.share_phrase_5, + R.string.share_phrase_6, + R.string.share_phrase_7, + R.string.share_phrase_8, + R.string.share_phrase_9, + R.string.share_phrase_10, + R.string.share_phrase_11, + R.string.share_phrase_12, + R.string.share_phrase_13, + R.string.share_phrase_14, + R.string.share_phrase_15 + ) + return this.getString( phraseIds.random(), songTitle, songArtist) +} \ No newline at end of file diff --git a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/ShareButton.kt b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/ShareButton.kt new file mode 100644 index 00000000..c5137e27 --- /dev/null +++ b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/ShareButton.kt @@ -0,0 +1,49 @@ +package io.newm.feature.musicplayer.share + +import android.content.Context +import android.content.Intent +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import io.newm.core.resources.R + +@Composable +fun ShareButton( + modifier: Modifier = Modifier, + songTitle: String? = null, + songArtist: String? = null, +) { + if (songTitle.isNullOrBlank() || songArtist.isNullOrBlank()) return + val context = LocalContext.current + IconButton(modifier = modifier, onClick = { + shareSong(context, songTitle, songArtist) + }) { + Icon( + painter = painterResource(id = R.drawable.ic_share), + contentDescription = "Share Song", + tint = Color.White + ) + } +} + +fun shareSong(context: Context, songTitle: String, songArtist: String) { + val randomPhrase = context.getRandomSharePhrase(songTitle, songArtist) + val callToAction = context.getString(R.string.share_call_to_action) + val shareText = """ + $randomPhrase + + $callToAction + """.trimIndent() + + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, shareText) + type = "text/plain" + } + val chooser = Intent.createChooser(shareIntent, "Share song via") + context.startActivity(chooser) +} \ No newline at end of file From 7b2c12e1143371398bfb4a88b5917541d42454fc Mon Sep 17 00:00:00 2001 From: Gohan Date: Wed, 25 Sep 2024 14:48:41 -0500 Subject: [PATCH 3/9] Update repeat button color states (#317) --- .../main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt index caae5d18..0de4d7eb 100644 --- a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt +++ b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt @@ -49,7 +49,6 @@ import io.newm.core.resources.R import io.newm.core.theme.Black import io.newm.core.theme.DarkPink import io.newm.core.theme.DarkViolet -import io.newm.core.theme.Gray23 import io.newm.core.theme.Gray500 import io.newm.core.theme.GraySuit import io.newm.core.theme.White @@ -360,7 +359,7 @@ fun RepeatButton( Icon( painter = painterResource(id = imageRes), contentDescription = "Repeat", - tint = if (repeatMode == PlaybackRepeatMode.REPEAT_OFF) Gray23 else White + tint = if (repeatMode == PlaybackRepeatMode.REPEAT_OFF) White else DarkViolet ) } } From c1af82cdcd9365308122c1cffc581f1aff3af648 Mon Sep 17 00:00:00 2001 From: Gohan Date: Wed, 25 Sep 2024 14:49:31 -0500 Subject: [PATCH 4/9] Display app and build version (#319) --- .../profile/ProfileBottomSheetLayout.kt | 31 ++++++++++++++++++- .../java/io/newm/core/ui/text/TextFields.kt | 7 +++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/android/app-newm/src/main/java/io/newm/screens/profile/ProfileBottomSheetLayout.kt b/android/app-newm/src/main/java/io/newm/screens/profile/ProfileBottomSheetLayout.kt index a2f92a1b..8f29f116 100644 --- a/android/app-newm/src/main/java/io/newm/screens/profile/ProfileBottomSheetLayout.kt +++ b/android/app-newm/src/main/java/io/newm/screens/profile/ProfileBottomSheetLayout.kt @@ -6,21 +6,27 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import io.newm.BuildConfig import io.newm.core.resources.R import io.newm.core.theme.Black90 import io.newm.core.theme.Gray400 import io.newm.core.ui.buttons.PrimaryButton import io.newm.core.ui.buttons.SecondaryButton +import io.newm.core.ui.text.versionTextStyle import io.newm.shared.public.analytics.NewmAppEventLogger import io.newm.shared.public.analytics.events.AppScreens @@ -71,7 +77,8 @@ fun ProfileBottomSheetLayout( text = stringResource(id = R.string.user_account_logout), onClick = onLogout ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(16.dp)) + AppVersion() } } }, @@ -79,3 +86,25 @@ fun ProfileBottomSheetLayout( content = content ) } + +@Composable +private fun AppVersion(){ + Column { + Text( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + text = "Version " + BuildConfig.VERSION_NAME, + style = versionTextStyle.copy(fontWeight = FontWeight.Bold) + ) + Text( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + text = "Build: " + BuildConfig.VERSION_CODE, + style = versionTextStyle + ) + } +} diff --git a/android/core/ui-utils/src/main/java/io/newm/core/ui/text/TextFields.kt b/android/core/ui-utils/src/main/java/io/newm/core/ui/text/TextFields.kt index a6b54f98..5aa4bee8 100644 --- a/android/core/ui-utils/src/main/java/io/newm/core/ui/text/TextFields.kt +++ b/android/core/ui-utils/src/main/java/io/newm/core/ui/text/TextFields.kt @@ -75,6 +75,13 @@ val formEmailStyle = TextStyle( color = Gray100 ) +val versionTextStyle = TextStyle( + fontSize = 12.sp, + fontFamily = inter, + fontWeight = FontWeight.Light, + color = Gray100 +) + object TextFieldWithLabelDefaults { object KeyboardOptions { @Stable From 3bf592d73e95c37e47a9f0cd9d5772ccdd115fe7 Mon Sep 17 00:00:00 2001 From: Gohan Date: Wed, 25 Sep 2024 15:19:58 -0500 Subject: [PATCH 5/9] Migrate upload artifact to v4 (#320) --- .github/workflows/android-preview-branch-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android-preview-branch-merge.yml b/.github/workflows/android-preview-branch-merge.yml index 3bf7cd6f..0e4b9d3f 100644 --- a/.github/workflows/android-preview-branch-merge.yml +++ b/.github/workflows/android-preview-branch-merge.yml @@ -58,7 +58,7 @@ jobs: env: BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: apk path: ${{steps.sign_app.outputs.signedReleaseFile}} From 3358bb09b32db4f645f721212126e159bb344f73 Mon Sep 17 00:00:00 2001 From: Gohan Date: Thu, 26 Sep 2024 19:07:39 -0500 Subject: [PATCH 6/9] Add client header (#321) --- .../kotlin/io.newm.shared/internal/api/LoginAPI.kt | 7 +++++++ shared/src/iosMain/kotlin/shared/actual.kt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/io.newm.shared/internal/api/LoginAPI.kt b/shared/src/commonMain/kotlin/io.newm.shared/internal/api/LoginAPI.kt index f2a5e370..4cb889c3 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/internal/api/LoginAPI.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/internal/api/LoginAPI.kt @@ -26,6 +26,7 @@ import io.newm.shared.internal.api.models.ResetPasswordException import io.newm.shared.internal.api.models.ResetPasswordRequest import io.newm.shared.internal.api.utils.addHumanVerificationCodeToHeader import io.newm.shared.public.models.error.KMMException +import shared.getPlatformName internal class LoginAPI( @@ -63,6 +64,7 @@ internal class LoginAPI( contentType(ContentType.Application.Json) setBody(user) addHumanVerificationCodeToHeader(humanVerificationCode) + parameter("clientPlatform", getPlatformName()) } when (response.status) { HttpStatusCode.OK -> {} @@ -85,6 +87,7 @@ internal class LoginAPI( contentType(ContentType.Application.Json) setBody(user) addHumanVerificationCodeToHeader(humanVerificationCode) + parameter("clientPlatform", getPlatformName()) }.body() suspend fun loginWithGoogle( @@ -95,6 +98,7 @@ internal class LoginAPI( contentType(ContentType.Application.Json) setBody(request) addHumanVerificationCodeToHeader(humanVerificationCode) + parameter("clientPlatform", getPlatformName()) } return when (response.status) { @@ -113,6 +117,7 @@ internal class LoginAPI( contentType(ContentType.Application.Json) setBody(request) addHumanVerificationCodeToHeader(humanVerificationCode) + parameter("clientPlatform", getPlatformName()) } return when (response.status) { @@ -127,6 +132,7 @@ internal class LoginAPI( val response = httpClient.post("/v1/auth/login/facebook") { contentType(ContentType.Application.Json) setBody(request) + parameter("clientPlatform", getPlatformName()) } return when (response.status) { @@ -141,6 +147,7 @@ internal class LoginAPI( val response = httpClient.post("/v1/auth/login/linkedin") { contentType(ContentType.Application.Json) setBody(request) + parameter("clientPlatform", getPlatformName()) } return when (response.status) { diff --git a/shared/src/iosMain/kotlin/shared/actual.kt b/shared/src/iosMain/kotlin/shared/actual.kt index 10f9fd00..cec0cb8e 100644 --- a/shared/src/iosMain/kotlin/shared/actual.kt +++ b/shared/src/iosMain/kotlin/shared/actual.kt @@ -20,4 +20,4 @@ actual fun platformModule() = module { single { TokenManagerImpl(get(), get()) } } -actual fun getPlatformName(): String = "iOS" +actual fun getPlatformName(): String = "IOS" From 61f80de2c239a2dcf27373ff75101eafc153f02e Mon Sep 17 00:00:00 2001 From: Gohan Date: Thu, 26 Sep 2024 21:42:51 -0500 Subject: [PATCH 7/9] Introduced Automatic Versioning with Manual Control Over Major Versions (#318) * Automate app versioning * Update version name --- android/app-newm/build.gradle.kts | 62 +++++++++++++++++++++++++++++-- shared/build.gradle.kts | 1 - 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/android/app-newm/build.gradle.kts b/android/app-newm/build.gradle.kts index cccb8199..7ceb1261 100644 --- a/android/app-newm/build.gradle.kts +++ b/android/app-newm/build.gradle.kts @@ -1,3 +1,5 @@ +import java.text.SimpleDateFormat +import java.util.Date apply(from = "../../gradle_include/compose.gradle") apply(from = "../../gradle_include/circuit.gradle") @@ -5,7 +7,7 @@ apply(from = "../../gradle_include/flipper.gradle") plugins { id("com.android.application") - id( "com.google.gms.google-services") + id("com.google.gms.google-services") id("kotlin-parcelize") kotlin("android") kotlin("kapt") @@ -23,8 +25,8 @@ android { applicationId = "io.newm" minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = 5 - versionName = "0.3.0" + versionCode = getCurrentDateTimeVersionCode() + versionName = getCustomVersionName(major = 1) testInstrumentationRunner = "io.newm.NewmAndroidJUnitRunner" testApplicationId = "io.newm.test" } @@ -60,7 +62,11 @@ android { dimension = "version" } all { - resValue("string", "account_type", "$applicationId${applicationIdSuffix.orEmpty()}.account") + resValue( + "string", + "account_type", + "$applicationId${applicationIdSuffix.orEmpty()}.account" + ) } } @@ -124,3 +130,51 @@ sentry { includeSourceContext.set(true) telemetry.set(true) } + + +/** + * Generates a version code based on the current date and time in the format `yyMMddHH`. + * + * The version code is an integer composed of: + * - `yy`: The last two digits of the current year. + * - `MM`: The current month. + * - `dd`: The current day of the month. + * - `HH`: The current hour (24-hour format). + * + * The function formats the current date and time using `SimpleDateFormat`, + * converts it into a string, and then parses it as an integer. + * + * @return An integer representing the current date and time in the format `yyMMddHH`. + */ +fun getCurrentDateTimeVersionCode(): Int { + val dateFormat = SimpleDateFormat("yyMMddHH") + return dateFormat.format(Date()).toInt() +} + +/** + * Generates a custom version name based on the provided major version and the current date and time. + * + * The version name follows the format: `major.YYYY.MMDDHHmm`, where: + * - `major`: The major version number passed as a parameter. + * - `YYYY`: The current year. + * - `MMDD`: The current month and day. + * - `HHmm`: The current hour and minute. + * + * The function retrieves the current date and time using `SimpleDateFormat` to format each component. + * + * @param major The major version number to be used as the first part of the version name. + * @return A custom version name string in the format: `major.YYYY.MMDDHHmm`. + */ +fun getCustomVersionName(major: Int): String { + val yearFormat = SimpleDateFormat("yyyy") + val monthDayFormat = SimpleDateFormat("MMdd") + val hourFormat = SimpleDateFormat("HH") + val minuteFormat = SimpleDateFormat("mm") + + val year = yearFormat.format(Date()) + val monthDay = monthDayFormat.format(Date()) + val hour = hourFormat.format(Date()) + val minute = minuteFormat.format(Date()) + + return "$major.$year.$monthDay$hour$minute" +} \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index cb370cc9..00b33949 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -116,7 +116,6 @@ buildConfig { buildConfigField("GOOGLE_AUTH_CLIENT_ID", properties.getProperty("GOOGLE_AUTH_CLIENT_ID").replace("\"", "")) buildConfigField("RECAPTCHA_SITE_KEY", properties.getProperty("RECAPTCHA_SITE_KEY").replace("\"", "")) buildConfigField("SENTRY_AUTH_TOKEN", properties.getProperty("SENTRY_AUTH_TOKEN").replace("\"", "")) - buildConfigField("NEWM_MOBILE_APP_VERSION", "0.0.0") buildConfigField("ANDROID_SENTRY_DSN", properties.getProperty("ANDROID_SENTRY_DSN").replace("\"", "")) } From 989734937131a8916b888819c9b396e66a8b7794 Mon Sep 17 00:00:00 2001 From: Gohan Date: Thu, 26 Sep 2024 22:18:04 -0500 Subject: [PATCH 8/9] Modify string for when a user shares a song (#322) --- .../resources/src/main/res/values/strings.xml | 32 +++++++++---------- .../feature/musicplayer/MusicPlayerViewer.kt | 1 - .../share/RandomPhraseGenerator.kt | 4 +-- .../feature/musicplayer/share/ShareButton.kt | 15 ++++----- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/android/core/resources/src/main/res/values/strings.xml b/android/core/resources/src/main/res/values/strings.xml index 7b081afc..4e181b3d 100644 --- a/android/core/resources/src/main/res/values/strings.xml +++ b/android/core/resources/src/main/res/values/strings.xml @@ -46,6 +46,7 @@ 1. Open your Cardano supported web3 wallet app 2. Use this URL to connect your wallet how to connect + "https://newm.io/app/" https://tools.newm.io/wallet-connect https://tools.newm.io Next @@ -79,22 +80,21 @@ Your new password Search Second Fragment - 👉 Tap in and discover what you\'re missing: https://newm.io - 🚀 Vibing to %1$s by %2$s on the Newm App! 🎧 Don\'t miss out! - 🎶 Can\'t stop grooving to %1$s by %2$s. Catch the beat on Newm! 🔥 - 🎧 Jammin\' to %1$s by %2$s on repeat! Newm\'s got the tunes! 🚀 - 🎉 Just found my new anthem: %1$s by %2$s! Discover yours on Newm! 🎶 - 🔥 Turn it up! %1$s by %2$s is a whole vibe on Newm App! 🎶 - 🌟 If you\'re not listening to %1$s by %2$s on Newm, you\'re missing out! 🎧 - 🎶 Locked into the rhythm of %1$s by %2$s on Newm App. Check it out! 🔥 - 🚀 Fueling my day with %1$s by %2$s. Newm App has all the jams! 🎧 - 🎉 Newm App’s playlist on fire with %1$s by %2$s. Ready to vibe? 🔥 - 🎧 Tuning into the best beats with %1$s by %2$s on Newm! 🚀 - 🔥 Crank up %1$s by %2$s on Newm App. The track is lit! 🎶 - 🎉 Found my groove with %1$s by %2$s. Discover fresh beats on Newm! 🚀 - 🎧 You NEED to hear %1$s by %2$s! Now playing on Newm App! 🔥 - 🎶 Feeling the rhythm of %1$s by %2$s. Newm App = non-stop vibes! 🚀 - 🔥 Plug into the sound of %1$s by %2$s on Newm App. Your new favorite jam awaits! 🎧 + Tune in to %1$s by %2$s on NEWM app! \n👉 Discover your next favorite song: %3$s + Find your rhythm with %1$s by %2$s on NEWM app. \n👉 Download NEWM app to start listening: %3$s + Bless your ears with %1$s by %2$s on NEWM app! \n👉 Download NEWM app to start listening: %3$s + %1$s by %2$s is the track you didn’t know you needed on NEWM app. \n👉 Download NEWM app to discover & listen: %3$s + Ready to refresh your playlist? %1$s by %2$s is live on NEWM app! \n👉 Check it out: %3$s + %1$s by %2$s is perfect for your next playlist. Stream it on NEWM app! \n👉 Discover it here: %3$s + If you\'re not listening to %1$s by %2$s, you\'re missing out! \n👉 Download NEWM app to discover & listen: %3$s + %1$s by %2$s is a whole vibe on NEWM app! \n👉 Discover your next playlist-worthy track: %3$s + Ready for a new vibe? %1$s by %2$s is waiting for you on NEWM app! \n👉 Discover more music on NEWM app: %3$s + Can\'t stop playing %1$s by %2$s? Stream it on NEWM app now! \n👉 Discover more: %3$s + Lose yourself in %1$s by %2$s! Download the NEWM app and start listening now. \n👉 Explore more music: %3$s + Welcome to your next music obsession – %1$s by %2$s! \nDownload the NEWM app and start listening now. \n👉 %3$s + Explore %1$s by %2$s on NEWM app and level-up your playlist! \n👉 Download NEWM app to discover & listen: %3$s + Love discovering new music? Start with %1$s by %2$s on NEWM app. \n👉 Download NEWM app to discover & listen: %3$s + Looking for fresh tracks? %1$s by %2$s is a must-listen on NEWM app! \n👉 Download NEWM app to discover & listen: %3$s Stars Earnings Followers this week diff --git a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt index 0de4d7eb..7085eb1e 100644 --- a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt +++ b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/MusicPlayerViewer.kt @@ -63,7 +63,6 @@ import io.newm.feature.musicplayer.models.PlaybackState import io.newm.feature.musicplayer.models.PlaybackStatus import io.newm.feature.musicplayer.models.Track import io.newm.feature.musicplayer.share.ShareButton -import io.newm.feature.musicplayer.share.getRandomSharePhrase import io.newm.feature.musicplayer.viewmodel.PlaybackUiEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/RandomPhraseGenerator.kt b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/RandomPhraseGenerator.kt index 4714a927..7f9a424c 100644 --- a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/RandomPhraseGenerator.kt +++ b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/RandomPhraseGenerator.kt @@ -3,7 +3,7 @@ package io.newm.feature.musicplayer.share import android.content.Context import io.newm.core.resources.R -fun Context.getRandomSharePhrase(songTitle: String, songArtist: String): String { +fun Context.getRandomSharePhrase(songTitle: String, songArtist: String, url: String): String { val phraseIds = listOf( R.string.share_phrase_1, R.string.share_phrase_2, @@ -21,5 +21,5 @@ fun Context.getRandomSharePhrase(songTitle: String, songArtist: String): String R.string.share_phrase_14, R.string.share_phrase_15 ) - return this.getString( phraseIds.random(), songTitle, songArtist) + return this.getString(phraseIds.random(), songTitle, songArtist, url) } \ No newline at end of file diff --git a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/ShareButton.kt b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/ShareButton.kt index c5137e27..110d83ee 100644 --- a/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/ShareButton.kt +++ b/android/features/music-player/src/main/java/io/newm/feature/musicplayer/share/ShareButton.kt @@ -31,17 +31,14 @@ fun ShareButton( } fun shareSong(context: Context, songTitle: String, songArtist: String) { - val randomPhrase = context.getRandomSharePhrase(songTitle, songArtist) - val callToAction = context.getString(R.string.share_call_to_action) - val shareText = """ - $randomPhrase - - $callToAction - """.trimIndent() - + val randomPhrase = context.getRandomSharePhrase( + songTitle, + songArtist, + context.getString(R.string.newm_download_app_landing_page) + ) val shareIntent = Intent().apply { action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, shareText) + putExtra(Intent.EXTRA_TEXT, randomPhrase) type = "text/plain" } val chooser = Intent.createChooser(shareIntent, "Share song via") From fcb5edf172891478ac612260c3eacdb42e1e943d Mon Sep 17 00:00:00 2001 From: Gohan Date: Fri, 27 Sep 2024 23:55:33 -0500 Subject: [PATCH 9/9] Aab action flow (#324) * Aab action flow * Updating name for new workflow * Updating name for new workflow * Updating name for new workflow * Update release location * bingo * Add more memory * Allocate more memory on properties file * Update to only run on release --- .../android-release-bundle-ontag.yml | 88 +++++++++++++++++++ gradle.properties | 2 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/android-release-bundle-ontag.yml diff --git a/.github/workflows/android-release-bundle-ontag.yml b/.github/workflows/android-release-bundle-ontag.yml new file mode 100644 index 00000000..3c40a311 --- /dev/null +++ b/.github/workflows/android-release-bundle-ontag.yml @@ -0,0 +1,88 @@ +name: Create AAB Release Bundle +on: + release: + types: [published] +jobs: + build_android_release_artifacts: + if: ${{ startsWith(github.event.release.tag_name, 'androidRelease') }} + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v2 + + - name: set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + + - name: Setup build tool version variable + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION + + - name: Setup local.properties + run: | + echo STAGING_URL="${{ vars.STAGING_URL }}" >> ./local.properties + echo PRODUCTION_URL="${{ vars.PRODUCTION_URL }}" >> ./local.properties + echo GOOGLE_AUTH_CLIENT_ID="${{ vars.GOOGLE_AUTH_CLIENT_ID }}" >> ./local.properties + + echo "RECAPTCHA_SITE_KEY=${{ secrets.RECAPTCHA_SITE_KEY }}" >> ./local.properties + echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> ./local.properties + echo "ANDROID_SENTRY_DSN=${{ secrets.ANDROID_SENTRY_DSN }}" >> ./local.properties + echo "auth.token=${{ secrets.ANDROID_SENTRY_AUTH_TOKEN}}" >> ./sentry.properties + + - name: create google-services.json + env: + GOOGLE_SERVICES_JSON: ${{ vars.GOOGLE_SERVICES_JSON }} + run: echo $GOOGLE_SERVICES_JSON > ./android/app-newm/google-services.json + + - name: build android apk + run: ./gradlew :android:app-newm:assembleProductionRelease + + - name: build bundle production + run: ./gradlew :android:app-newm:bundleProduction + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + # ID used to access action output + id: sign_app + with: + releaseDirectory: android/app-newm/build/outputs/apk/production/release + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + alias: ${{ secrets.KEY_ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + env: + BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} + + - uses: r0adkll/sign-android-release@v1 + name: Sing app bundle + # ID used to access action output + id: sign_aab + with: + releaseDirectory: android/app-newm/build/outputs/bundle/productionRelease + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + alias: ${{ secrets.KEY_ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + env: + BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} + + - uses: actions/upload-artifact@v4 + with: + name: apk + path: ${{steps.sign_app.outputs.signedReleaseFile}} + + - uses: actions/upload-artifact@v4 + with: + name: aab + path: ${{steps.sign_aab.outputs.signedReleaseFile}} + + - name: Upload artifact to Firebase App Distribution + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{secrets.FIREBASE_ANDROID_APP_ID}} + serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }} + groups: newm-team + file: ${{steps.sign_app.outputs.signedReleaseFile}} diff --git a/gradle.properties b/gradle.properties index 50bdf061..a0ae98b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ #Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4096M" org.gradle.caching=true org.gradle.configuration-cache=true