Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Commit Lint (Semantic)

on:
push:
branches: [master, 'feat/**', 'fix/**', 'chore/**', 'ci/**']
pull_request:
types: [opened, synchronize, reopened, edited]
branches: [master]

permissions:
contents: read
pull-requests: read

concurrency:
group: commitlint-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

jobs:
commitlint:
name: Enforce conventional commits
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'

- name: Install commitlint
run: npm install --no-save @commitlint/cli@19 @commitlint/config-conventional@19

- name: Lint PR commits
if: github.event_name == 'pull_request'
run: |
npx commitlint \
--from "${{ github.event.pull_request.base.sha }}" \
--to "${{ github.event.pull_request.head.sha }}" \
--verbose

- name: Lint push commits
if: github.event_name == 'push'
run: |
# Validate every new commit on the push (range from before → after).
BEFORE="${{ github.event.before }}"
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
npx commitlint --last --verbose
else
npx commitlint --from "$BEFORE" --to "${{ github.sha }}" --verbose
fi

- name: Lint PR title
if: github.event_name == 'pull_request'
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "$PR_TITLE" | npx commitlint --verbose
211 changes: 211 additions & 0 deletions .github/workflows/release-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
name: Release APK

# Builds a signed (CI keystore) release APK and publishes a GitHub Release with
# conventional-commit feature notes. Triggered by version tags or manually.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Release version without v prefix (e.g. 0.8.0-soundcloud)'
required: true
type: string
prerelease:
description: 'Mark as pre-release'
required: false
default: true
type: boolean

permissions:
contents: write

concurrency:
group: release-apk-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.meta.outputs.version }}
tag: ${{ steps.meta.outputs.tag }}

steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Resolve version metadata
id: meta
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
VERSION="${VERSION#v}"
TAG="v${VERSION}"
else
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "Resolved release ${TAG}"

- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6

- name: Cache keystore
id: cache-keystore
uses: actions/cache@v5
with:
path: vz-pixelplay.jks
key: ${{ runner.os }}-keystore-vz-pixelplay

- name: Generate keystore if not cached
if: steps.cache-keystore.outputs.cache-hit != 'true'
run: |
keytool -genkey -v -keystore vz-pixelplay.jks -alias pixelplay-key -keyalg RSA -keysize 4096 -validity 10000 \
-storepass 994273 -keypass 994273 \
-dname "CN=PixelPlay, OU=Dev, O=PixelPlay, L=World, S=World, C=US"

- name: Create keystore.properties
run: |
echo "storePassword=994273" > keystore.properties
echo "keyAlias=pixelplay-key" >> keystore.properties
echo "keyPassword=994273" >> keystore.properties

- name: Build Phone release APKs
run: gradle :app:assembleRelease -Ppixelplay.enableAbiSplits=true

- name: Verify APKs
run: |
BUILD_TOOLS_VERSION="$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -n 1)"
for apk in \
app/build/outputs/apk/release/app-arm64-v8a-release.apk \
app/build/outputs/apk/release/app-armeabi-v7a-release.apk
do
"$ANDROID_HOME/build-tools/$BUILD_TOOLS_VERSION/aapt2" dump badging "$apk" >/dev/null
"$ANDROID_HOME/build-tools/$BUILD_TOOLS_VERSION/apksigner" verify --verbose "$apk"
done

- name: Stage release assets
run: |
mkdir -p dist
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk \
"dist/PixelPlayer-${{ steps.meta.outputs.version }}-arm64-v8a.apk"
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk \
"dist/PixelPlayer-${{ steps.meta.outputs.version }}-armeabi-v7a.apk"

- name: Generate release notes from conventional commits
id: notes
uses: actions/github-script@v9
with:
script: |
const { execSync } = require('child_process');
const tag = '${{ steps.meta.outputs.tag }}';
const version = '${{ steps.meta.outputs.version }}';

let range = '';
try {
const tags = execSync('git tag --sort=-creatordate', { encoding: 'utf8' })
.split('\n')
.map((t) => t.trim())
.filter(Boolean)
.filter((t) => t !== tag);
if (tags.length > 0) {
range = `${tags[0]}..HEAD`;
}
} catch (_) {}

const logCmd = range
? `git log ${range} --pretty=format:%s`
: 'git log -30 --pretty=format:%s';
const subjects = execSync(logCmd, { encoding: 'utf8' })
.split('\n')
.map((s) => s.trim())
.filter(Boolean);

const buckets = {
feat: [],
fix: [],
perf: [],
refactor: [],
docs: [],
chore: [],
other: [],
};

const re = /^(feat|fix|perf|refactor|docs|chore|ci|build|test|style|revert)(\(.+\))?!?:\s*(.+)$/i;
for (const subject of subjects) {
const m = subject.match(re);
if (!m) {
buckets.other.push(subject);
continue;
}
const type = m[1].toLowerCase();
const body = m[3];
if (buckets[type]) {
buckets[type].push(body);
} else {
buckets.other.push(subject);
}
}

const section = (title, items) => {
if (!items.length) return '';
return `### ${title}\n\n${items.map((i) => `- ${i}`).join('\n')}\n\n`;
};

let body = `## PixelPlayer ${version}\n\n`;
body += section('Features', buckets.feat);
body += section('Fixes', buckets.fix);
body += section('Performance', buckets.perf);
body += section('Refactors', buckets.refactor);
body += section('Docs', buckets.docs);
body += section('Chores', buckets.chore);
body += section('Other', buckets.other);
body += [
'### Downloads',
'',
'- `PixelPlayer-*-arm64-v8a.apk` — modern 64-bit devices (recommended)',
'- `PixelPlayer-*-armeabi-v7a.apk` — older 32-bit devices',
'',
'> CI builds use the repository CI keystore (not the store/Play signing key).',
'',
].join('\n');

core.setOutput('body', body);
await core.summary.addHeading(`Release ${tag}`).addRaw(body).write();

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.meta.outputs.tag }}
name: PixelPlayer ${{ steps.meta.outputs.version }}
body: ${{ steps.notes.outputs.body }}
draft: false
prerelease: ${{ github.event_name == 'workflow_dispatch' && inputs.prerelease || contains(steps.meta.outputs.version, '-') }}
files: |
dist/PixelPlayer-${{ steps.meta.outputs.version }}-arm64-v8a.apk
dist/PixelPlayer-${{ steps.meta.outputs.version }}-armeabi-v7a.apk
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload artifacts
uses: actions/upload-artifact@v7.0.1
with:
name: PixelPlayer-${{ steps.meta.outputs.version }}-release-apks
path: dist/*.apk
if-no-files-found: error
compression-level: 0
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ android {
?: "b18441a1ff607e10a989891a5462e627"
buildConfigField("int", "TELEGRAM_API_ID", telegramApiId)
buildConfigField("String", "TELEGRAM_API_HASH", "\"$telegramApiHash\"")

val soundCloudClientId = localProperties.getProperty("soundcloud.clientId").orEmpty()
.replace("\\", "\\\\")
.replace("\"", "\\\"")
buildConfigField("String", "SOUNDCLOUD_CLIENT_ID", "\"$soundCloudClientId\"")
}

signingConfigs {
Expand Down Expand Up @@ -322,6 +327,7 @@ dependencies {
implementation(libs.androidx.security.crypto)
implementation(libs.google.play.services.cast.framework)
implementation(libs.tdlib)
implementation(libs.newpipe.extractor)

// UI Utilities & Extra
implementation(libs.timber)
Expand Down
14 changes: 14 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,17 @@
public static int d(...);
public static int i(...);
}

# NewPipe Extractor (SoundCloud)
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.javascript.XMLLib.** { *; }
-dontwarn org.mozilla.javascript.XMLLib
-dontwarn org.schabi.newpipe.extractor.**

# Rhino / NewPipe optional JDK APIs not on Android
-dontwarn org.mozilla.javascript.**
-dontwarn javax.script.**
-dontwarn java.beans.**
-dontwarn jdk.dynalink.**
-dontwarn org.mozilla.classfile.**
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@
android:theme="@style/Theme.PixelPlay"
android:windowSoftInputMode="adjustResize" />

<activity
android:name=".presentation.soundcloud.auth.SoundCloudLoginActivity"
android:exported="false"
android:label="SoundCloud Login"
android:theme="@style/Theme.PixelPlay"
android:windowSoftInputMode="adjustResize" />


<activity
android:name=".ExternalPlayerActivity"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/theveloper/pixelplay/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,8 @@ class MainActivity : ComponentActivity() {
persistentListOf(
BottomNavItem("Home", R.string.nav_bar_home, R.drawable.rounded_home_24, R.drawable.home_24_rounded_filled, Screen.Home),
BottomNavItem("Search", R.string.nav_bar_search, R.drawable.rounded_search_24, R.drawable.rounded_search_24, Screen.Search),
BottomNavItem("Library", R.string.nav_bar_library, R.drawable.rounded_library_music_24, R.drawable.round_library_music_24, Screen.Library)
BottomNavItem("Library", R.string.nav_bar_library, R.drawable.rounded_library_music_24, R.drawable.round_library_music_24, Screen.Library),
BottomNavItem("SoundCloud", R.string.nav_bar_soundcloud, R.drawable.ic_soundcloud_24, R.drawable.ic_soundcloud_filled_24, Screen.SoundCloud),
)
}
val navBackStackEntry by navController.currentBackStackEntryAsState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import com.theveloper.pixelplay.presentation.screens.RecentlyPlayedScreen
import com.theveloper.pixelplay.presentation.screens.AboutScreen
import com.theveloper.pixelplay.presentation.screens.OpenSourceLicensesScreen
import com.theveloper.pixelplay.presentation.screens.SearchScreen
import com.theveloper.pixelplay.presentation.screens.SoundCloudScreen
import com.theveloper.pixelplay.presentation.screens.StatsScreen
import com.theveloper.pixelplay.presentation.screens.SettingsScreen
import com.theveloper.pixelplay.presentation.screens.SettingsCategoryScreen
Expand Down Expand Up @@ -205,6 +206,45 @@ fun AppNavigation(
LibraryScreen(navController = navController, playerViewModel = playerViewModel)
}
}
composable(
Screen.SoundCloud.route,
enterTransition = {
mainRootEnterTransition(
fromRoute = initialState.destination.route,
toRoute = targetState.destination.route,
fallback = enterTransition()
)
},
exitTransition = {
mainRootExitTransition(
fromRoute = initialState.destination.route,
toRoute = targetState.destination.route,
fallback = exitTransition()
)
},
popEnterTransition = {
mainRootEnterTransition(
fromRoute = initialState.destination.route,
toRoute = targetState.destination.route,
fallback = popEnterTransition()
)
},
popExitTransition = {
mainRootExitTransition(
fromRoute = initialState.destination.route,
toRoute = targetState.destination.route,
fallback = popExitTransition()
)
},
) {
ScreenWrapper(navController = navController, playerViewModel = playerViewModel, animatedVisibilityScope = this) {
SoundCloudScreen(
playerViewModel = playerViewModel,
navController = navController,
paddingValues = paddingValues,
)
}
}
composable(
Screen.Settings.route,
) {
Expand Down
Loading