Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1c46ef7
build: Initial Jellyfin v12 support
MakD Aug 30, 2026
bd8f3d5
build: add SLF4J API dependency
MakD Aug 30, 2026
135e3a2
refactor: update Jellyfin SDK API class names and playback reporting …
MakD Aug 30, 2026
7b0b3d9
feat: add predictive back animations to main navigation
MakD Aug 30, 2026
ac63522
build: add kotlin-logging dependency
MakD Aug 30, 2026
981a3a9
feat: implement interactive in-app log viewer and live console
MakD Aug 30, 2026
0bc7557
feat: enhance server management, media metadata display, and music pl…
MakD Aug 30, 2026
ff4599e
fix: ensure correct draw scope for lyrics progress rendering
MakD Aug 30, 2026
4539079
feat: introduce unified UserAvatar component and refactor profile UI …
MakD Aug 31, 2026
50ce9d0
feat: implement "forget account" functionality to clear saved user data
MakD Aug 31, 2026
9cbda75
refactor: centralize server URL parsing and improve server management UI
MakD Aug 31, 2026
e906480
refactor: improve service tile layout and bottom sheet inset handling
MakD Aug 31, 2026
0f0b576
feat: enhance lyrics caching logic and refine continue watching filters
MakD Aug 31, 2026
de2c99a
feat: support separate views for album artists and all artists in mus…
MakD Aug 31, 2026
1455f68
perf: optimize item detail loading and prevent redundant network fetches
MakD Aug 31, 2026
26be9dc
perf: optimize show background sync and eager load next up item
MakD Sep 1, 2026
4933c54
feat: add Quick Connect sign-in support for Jellyseerr
MakD Sep 1, 2026
9742317
perf: optimize image loading, blurhash decoding, and network timeouts
MakD Sep 1, 2026
e1b9dc5
fix: adjust SdkLogBridge log level based on build configuration
MakD Sep 1, 2026
1c52aeb
feat: lazy load library filter options and improve filter sheet UI
MakD Sep 1, 2026
2a83364
feat: add user setting for toggling landscape side sheet layout
MakD Sep 1, 2026
8b16478
feat: filter music genres and artists by parent library ID
MakD Sep 1, 2026
2989e73
feat: enhance music screens with related content, external links, and…
MakD Sep 2, 2026
701c672
feat: implement remote session control and remote message handling
MakD Sep 2, 2026
fcb0781
perf: optimize EPG rendering, state flows, and Live TV guide querying
MakD Sep 2, 2026
bfa9dfb
fix: update player controls window insets to use safe drawing
MakD Sep 2, 2026
c6b2589
feat: add dynamic device profiles, streaming quality options, and tra…
MakD Sep 3, 2026
4468b80
refactor: standardize audio player controls and introduce overflow op…
MakD Sep 3, 2026
ad82647
feat: add Audiobookshelf bookmarks support and standardize landscape …
MakD Sep 3, 2026
111129d
refactor: reorganize settings structure and introduce playback option…
MakD Sep 3, 2026
006e3e4
refactor: standardize destructive action tints and eager-load bookmarks
MakD Sep 3, 2026
f79a132
feat: overhaul downloads screen with grouped catalog grid and multi-s…
MakD Sep 3, 2026
8dd305c
refactor: improve home screen refresh strategy and spotlight layout c…
MakD Sep 3, 2026
86338f6
fix: improve HDR capability detection, transcode fallback, and log re…
MakD Sep 4, 2026
5a974b3
perf: coalesce playback section refreshes and streamline media change…
MakD Sep 4, 2026
6357898
feat: implement stream resolution and audio transcoding for music pla…
MakD Sep 4, 2026
56bc70f
fix: refine music playback stats tracking and thread safety in AudioS…
MakD Sep 4, 2026
9ac7fc4
fix: preserve AudioService during playback engine switch
MakD Sep 4, 2026
381441e
feat: add audio format and transcoding indicators to player UI
MakD Sep 4, 2026
977c8bd
ci: enable multi-branch support and dynamic tagging for nightly builds
MakD Sep 4, 2026
d7d40ab
feat: implement mDNS-based local service discovery for media servers
MakD Sep 4, 2026
e21dbe6
feat: add log viewer and improve adaptive navigation in settings
MakD Sep 5, 2026
1f89d92
feat: enhance log viewer with search, filtering, and crash report man…
MakD Sep 5, 2026
d1d60e2
feat: implement advanced sleep timer and refactor download storage ma…
MakD Sep 6, 2026
0c82acc
fix: improve player overlay inset management and episode switcher pro…
MakD Sep 6, 2026
ee2eeac
feat: implement support for ACCESS_LOCAL_NETWORK permission
MakD Sep 6, 2026
4727f34
style: reformat codebase with ktfmt and enhance build artifacts
MakD Sep 6, 2026
9f0a863
chore: enable Room schema export and implement database migration tes…
MakD Sep 6, 2026
05520e2
chore: refactor for structured concurrency, dependency cleanup, and U…
MakD Sep 15, 2026
0b9ff56
feat: implement music "Never transcode" setting and refactor player q…
MakD Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
root = true

[*.{kt,kts}]
ij_kotlin_imports_layout = *
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

on:
push:
pull_request:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
check:
name: Format + compile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

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

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

- name: Make gradlew executable
run: chmod +x gradlew

- name: Check formatting
run: ./gradlew ktfmtCheck

- name: Check cancellation rethrows
if: always()
run: ./gradlew checkCancellationRethrow

- name: Compile
if: always()
run: ./gradlew compileDebugKotlin

- name: Unit tests
if: always()
run: ./gradlew testDebugUnitTest
86 changes: 73 additions & 13 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,29 @@ jobs:
DATE=$(TZ=Asia/Kolkata date +%Y-%m-%d)
echo "date=${DATE}" >> $GITHUB_OUTPUT

if gh release view nightly > /dev/null 2>&1; then
echo "Comparing commits since last nightly"
COMMITS=$(git log nightly..HEAD --oneline --no-merges -- \
BRANCH="${GITHUB_REF_NAME}"
if [ "$BRANCH" = "master" ]; then
TAG="nightly"
TITLE="Nightly ${DATE}"
else
SLUG=$(echo "$BRANCH" | sed 's#[^a-zA-Z0-9._-]#-#g')
TAG="nightly-${SLUG}"
TITLE="Nightly ${DATE} (${BRANCH})"
fi
echo "Branch ${BRANCH} -> tag ${TAG}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "title=${TITLE}" >> $GITHUB_OUTPUT

if gh release view "$TAG" > /dev/null 2>&1; then
echo "Comparing commits since last ${TAG}"
COMMITS=$(git log "${TAG}..HEAD" --oneline --no-merges -- \
app/src/ app/build.gradle.kts gradle/ build.gradle.kts settings.gradle.kts)
elif [ "$BRANCH" != "master" ] && git rev-parse --verify origin/master > /dev/null 2>&1; then
echo "First ${TAG} — using every commit since ${BRANCH} diverged from master"
COMMITS=$(git log origin/master..HEAD --oneline --no-merges -- \
app/src/ app/build.gradle.kts gradle/ build.gradle.kts settings.gradle.kts)
else
echo "No previous nightly found, using last 15 commits"
echo "No previous ${TAG} found, using last 15 commits"
COMMITS=$(git log -15 --oneline --no-merges -- \
app/src/ app/build.gradle.kts gradle/ build.gradle.kts settings.gradle.kts)
fi
Expand All @@ -82,6 +99,16 @@ jobs:
echo "$COMMITS" >> $GITHUB_OUTPUT
echo "$DELIMITER" >> $GITHUB_OUTPUT

NOTICE_DELIMITER="EOF_NOTICE_$(date +%s)"
echo "notice<<$NOTICE_DELIMITER" >> $GITHUB_OUTPUT
if [ "$BRANCH" != "master" ]; then
echo "> [!CAUTION]" >> $GITHUB_OUTPUT
echo "> Built from the \`${BRANCH}\` branch, not \`master\`." >> $GITHUB_OUTPUT
echo "> **This build targets Jellyfin 12 and breaks compatibility with Jellyfin 10 servers.**" >> $GITHUB_OUTPUT
echo "> Future releases will drop v10 support entirely. Do not install this against a v10 server." >> $GITHUB_OUTPUT
fi
echo "$NOTICE_DELIMITER" >> $GITHUB_OUTPUT

- name: Decode keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.jks
Expand All @@ -104,27 +131,41 @@ jobs:
- name: Rename APKs
run: |
DATE="${{ steps.info.outputs.date }}"
TAG="${{ steps.info.outputs.tag }}"
for APK in app/build/outputs/apk/nightly/*.apk; do
ABI=$(basename "$APK" | grep -oP '(arm64-v8a|armeabi-v7a)')
cp "$APK" "AFinity-nightly-${DATE}-${ABI}.apk"
cp "$APK" "AFinity-${TAG}-${DATE}-${ABI}.apk"
done

- name: Package R8 mapping
run: |
DATE="${{ steps.info.outputs.date }}"
TAG="${{ steps.info.outputs.tag }}"
MAPPING="app/build/outputs/mapping/nightly/mapping.txt"
if [ ! -f "$MAPPING" ]; then
echo "::warning::No R8 mapping at ${MAPPING}. Publishing without one; library frames in crashes from this build will not be retraceable."
exit 0
fi
gzip -9 -c "$MAPPING" > "AFinity-${TAG}-${DATE}-mapping.txt.gz"
ls -lh "AFinity-${TAG}-${DATE}-mapping.txt.gz"

- name: Delete existing nightly release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if gh release view nightly > /dev/null 2>&1; then
echo "Deleting existing nightly release..."
gh release delete nightly --yes --cleanup-tag
TAG="${{ steps.info.outputs.tag }}"
if gh release view "$TAG" > /dev/null 2>&1; then
echo "Deleting existing ${TAG} release..."
gh release delete "$TAG" --yes --cleanup-tag
else
echo "No existing nightly release found"
echo "No existing ${TAG} release found"
fi

- name: Publish pre-release
uses: softprops/action-gh-release@v2
with:
tag_name: nightly
name: Nightly ${{ steps.info.outputs.date }}
tag_name: ${{ steps.info.outputs.tag }}
name: ${{ steps.info.outputs.title }}
prerelease: true
body: |
**Automated nightly build** — ${{ steps.info.outputs.date }} (IST)
Expand All @@ -135,12 +176,31 @@ jobs:
```
> [!CAUTION]
> ⚠️ Nightly builds may be unstable. Expect Bugs. Installs alongside the beta release.
files: AFinity-nightly-${{ steps.info.outputs.date }}-*.apk

${{ steps.info.outputs.notice }}

<details><summary>Deobfuscating a crash from this build</summary>

Download `AFinity-${{ steps.info.outputs.tag }}-${{ steps.info.outputs.date }}-mapping.txt.gz`,
then:

```
gunzip AFinity-*-mapping.txt.gz
retrace mapping.txt stacktrace.txt
```

The mapping only matches **this** nightly — each nightly replaces the previous one.

</details>
fail_on_unmatched_files: false
files: |
AFinity-${{ steps.info.outputs.tag }}-${{ steps.info.outputs.date }}-*.apk
AFinity-${{ steps.info.outputs.tag }}-${{ steps.info.outputs.date }}-mapping.txt.gz

- name: Notify Discord
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run discord-release-notification.yml \
--ref master \
-f release_tag=nightly
-f release_tag=${{ steps.info.outputs.tag }}
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

135 changes: 133 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import java.util.Properties
import java.util.regex.Pattern
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension

plugins {
alias(libs.plugins.android.application)
Expand All @@ -9,6 +12,8 @@ plugins {
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.hilt.android)
alias(libs.plugins.ksp)
alias(libs.plugins.room)
alias(libs.plugins.androidx.baselineprofile)
id("kotlin-parcelize")
}

Expand All @@ -30,6 +35,15 @@ aboutLibraries {
}
}

configure<ComposeCompilerGradlePluginExtension> {
stabilityConfigurationFiles.add(layout.projectDirectory.file("compose_compiler_config.conf"))

if (providers.gradleProperty("composeMetrics").orNull == "true") {
metricsDestination.set(layout.buildDirectory.dir("compose-metrics"))
reportsDestination.set(layout.buildDirectory.dir("compose-reports"))
}
}

configure<ApplicationExtension> {
namespace = "com.makd.afinity"
compileSdk = 37
Expand Down Expand Up @@ -129,19 +143,46 @@ configure<ApplicationExtension> {
}
}

room { schemaDirectory("$projectDir/schemas") }

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-Xjvm-default=all",
"-Xcontext-parameters",
"-jvm-default=no-compatibility",
)
}
}

configure<ApplicationAndroidComponentsExtension> {
onVariants { variant ->
val buildType = variant.buildType ?: return@onVariants
if (buildType != "release" && buildType != "nightly") return@onVariants

val mappingFile = variant.artifacts.get(SingleArtifact.OBFUSCATION_MAPPING_FILE)
val variantName = variant.name.replaceFirstChar { it.uppercase() }

val archiveMapping =
tasks.register<Zip>("archive${variantName}Mapping") {
group = "reporting"
description = "Archives the R8 mapping for $buildType builds."
from(mappingFile)
archiveFileName.set(
"afinity-v$appVersionName-$appVersionCode-${variant.name}-mapping.zip"
)
destinationDirectory.set(rootProject.layout.projectDirectory.dir("mapping-archive"))
onlyIf { mappingFile.orNull?.asFile?.exists() == true }
}

tasks
.matching { it.name == "assemble$variantName" }
.configureEach { finalizedBy(archiveMapping) }
}
}

dependencies {
coreLibraryDesugaring(libs.android.desugar.jdk)
debugImplementation(libs.leakcanary.android)
Expand Down Expand Up @@ -195,19 +236,22 @@ dependencies {
implementation(libs.compose.pager.indicator)
implementation(libs.hilt.android)
implementation(libs.jellyfin.core)
implementation(libs.kotlin.logging)
implementation(libs.kotlinx.serialization.json)
implementation(libs.libmpv)
implementation(libs.lottie.compose)
implementation(libs.media3.ffmpeg.decoder)
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
implementation(libs.androidx.profileinstaller)
implementation(libs.play.services.cast.framework)
implementation(libs.reorderable)
implementation(libs.retrofit)
implementation(libs.retrofit.kotlinx.serialization)
implementation(libs.richtext.commonmark)
implementation(libs.richtext.ui)
implementation(libs.richtext.ui.material3)
implementation(libs.slf4j.api)
implementation(libs.socketio) { exclude(group = "org.json", module = "json") }
implementation(libs.timber)
implementation(libs.tink.android)
Expand All @@ -216,4 +260,91 @@ dependencies {
ksp(libs.androidx.room.compiler)
ksp(libs.hilt.android.compiler)
ksp(libs.kotlin.metadata.jvm)

androidTestImplementation(libs.androidx.room.testing)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.junit)

testImplementation(libs.junit)

baselineProfile(project(":benchmark"))
}

val cancellationRethrowAllowlist =
setOf(
"data/database/AfinityTypeConverters.kt",
"data/manager/DownloadNotificationManager.kt",
"data/models/jellyseerr/JellyseerrRequest.kt",
"data/models/jellyseerr/SearchResultItem.kt",
"data/repository/audiobookshelf/AbsProgressSyncScheduler.kt",
"data/repository/audiobookshelf/AudiobookshelfEpisodeMappers.kt",
"data/storage/StorageLocationProvider.kt",
"data/sync/UserDataSyncScheduler.kt",
"di/NetworkModule.kt",
"player/common/AudioEqualizerManager.kt",
"player/mpv/MPVPlayer.kt",
"ui/audiobookshelf/libraries/AudiobookshelfLibrariesViewModel.kt",
"ui/components/AsyncImage.kt",
"ui/components/HeroCarousel.kt",
"ui/person/PersonViewModel.kt",
"ui/player/Extensions.kt",
"ui/player/PlayerActivity.kt",
"ui/player/utils/VolmeManager.kt",
"ui/requests/SeerrMediaDetailScreen.kt",
"ui/settings/player/PlayerOptionsScreen.kt",
"ui/settings/servers/utils/Formatters.kt",
"ui/utils/IntentUtils.kt",
)

tasks.register("checkCancellationRethrow") {
group = "verification"
description =
"Fails if a catch (e: Exception) is missing a CancellationException rethrow above it."

val sourceRoot = layout.projectDirectory.dir("src/main/java/com/makd/afinity").asFile
val allowlist = cancellationRethrowAllowlist
inputs.dir(sourceRoot)

doLast {
val violations = mutableListOf<String>()
sourceRoot
.walkTopDown()
.filter { it.isFile && it.extension == "kt" }
.forEach { file ->
val relative = file.relativeTo(sourceRoot).invariantSeparatorsPath
if (relative in allowlist) return@forEach
val lines = file.readLines()
lines.forEachIndexed { index, line ->
if (!line.contains("catch (e: Exception)")) return@forEachIndexed
val guarded =
index >= 2 &&
lines[index - 2].contains("catch (e: CancellationException)") &&
lines[index - 1].trim() == "throw e"
if (!guarded) violations += "$relative:${index + 1}"
}
}

if (violations.isNotEmpty()) {
throw GradleException(
buildString {
appendLine(
"Cancellation rethrow missing at ${violations.size} site(s). " +
"Catching Exception swallows coroutine cancellation, so the coroutine " +
"keeps running until its next suspension point."
)
violations.forEach { appendLine(" $it") }
appendLine()
appendLine("Add directly above each catch:")
appendLine(" } catch (e: CancellationException) {")
appendLine(" throw e")
appendLine()
appendLine(
"If the file contains no coroutines, add it to " +
"cancellationRethrowAllowlist in app/build.gradle.kts instead."
)
}
)
}
}
}
Loading
Loading