Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b761be1
chore: Add extended Material Icons dependency
MakD Feb 4, 2026
1d7dccf
feat(audiobookshelf): Add Audiobookshelf integration
MakD Feb 4, 2026
adf55a9
refactor(UI): Adjust MiniPlayer layout for proper display
MakD Feb 4, 2026
1f78615
refactor(UI): Replace top app bar in Audiobookshelf screen
MakD Feb 4, 2026
27e5fe6
feat(audiobookshelf): Overhaul libraries screen with tabbed navigation
MakD Feb 4, 2026
f44386f
refactor(UI): Redesign item details screen with hero header and lands…
MakD Feb 4, 2026
9264022
feat(UI): Display narrator in audiobook item header
MakD Feb 4, 2026
ff0bd9b
feat(audiobook): Add background playback and media notifications
MakD Feb 5, 2026
1357a5d
refactor(player): Redesign Audiobookshelf player UI and underlying se…
MakD Feb 5, 2026
18df8af
refactor(icons): Update and add new icons for Audiobookshelf UI
MakD Feb 6, 2026
a6ddb52
feat(player): Add extensive logging for Audiobookshelf playback
MakD Feb 6, 2026
e720557
fix(audiobookshelf): Fetch all items from paginated endpoints
MakD Feb 6, 2026
56500e5
refactor(Audiobookshelf): Sort library items by title and simplify ma…
MakD Feb 6, 2026
ec53e98
refactor(UI): Update icons for Overseerr, Audiobookshelf, and Playbac…
MakD Feb 6, 2026
9d18563
chore: Add and configure ktfmt for code formatting
MakD Feb 6, 2026
a10e85a
style: Format code with trailing commas
MakD Feb 6, 2026
f20a423
refactor(audiobookshelf): Overhaul login and add series/genre screens
MakD Feb 7, 2026
d965d53
refactor(UI): Remove redundant back buttons on media detail screens
MakD Feb 7, 2026
e17199c
feat(audiobookshelf): Add genre sections to the home screen
MakD Feb 7, 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
  •  
  •  
  •  
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.

1 change: 0 additions & 1 deletion .idea/misc.xml

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

11 changes: 6 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ android {
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
if (variant.buildType.name == "release") {
val outputFileName = "afinity-v${variant.versionName}-${output.getFilter("ABI")}.apk"
val outputFileName =
"afinity-v${variant.versionName}-${output.getFilter("ABI")}.apk"
output.outputFileName = outputFileName
}
}
Expand All @@ -64,7 +65,7 @@ android {
buildConfigField("boolean", "DEBUG", "false")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
}
}
Expand All @@ -91,7 +92,7 @@ android {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.material3.pulltorefresh.ExperimentalMaterial3PullToRefreshApi",
"-Xjvm-default=all",
"-Xcontext-receivers"
"-Xcontext-receivers",
)
}
}
Expand All @@ -116,14 +117,14 @@ android {
}

dependencies {

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.ui)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.palette.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
Expand Down Expand Up @@ -178,4 +179,4 @@ dependencies {
implementation(libs.androidx.paging.runtime)
implementation(libs.androidx.paging.compose)
coreLibraryDesugaring(libs.android.desugar.jdk)
}
}
14 changes: 13 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application
android:name=".AfinityApplication"
Expand All @@ -29,7 +31,8 @@
android:exported="true"
android:label="@string/app_name"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.AFinity.Splash">
android:theme="@style/Theme.AFinity.Splash"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down Expand Up @@ -70,6 +73,15 @@
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />

<service
android:name=".player.audiobookshelf.AudiobookshelfPlayerService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
</application>

</manifest>
20 changes: 7 additions & 13 deletions app/src/main/java/com/makd/afinity/AfinityApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,25 @@ import com.makd.afinity.data.repository.PreferencesRepository
import com.makd.afinity.data.updater.UpdateScheduler
import com.makd.afinity.data.updater.models.UpdateCheckFrequency
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okio.Path.Companion.toOkioPath
import timber.log.Timber
import javax.inject.Inject

@HiltAndroidApp
class AfinityApplication : Application(), Configuration.Provider, SingletonImageLoader.Factory {

@Inject
lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var workerFactory: HiltWorkerFactory

@Inject
lateinit var updateScheduler: UpdateScheduler
@Inject lateinit var updateScheduler: UpdateScheduler

@Inject
lateinit var preferencesRepository: PreferencesRepository
@Inject lateinit var preferencesRepository: PreferencesRepository

@Inject
lateinit var okHttpClient: OkHttpClient
@Inject lateinit var okHttpClient: OkHttpClient

private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

Expand All @@ -60,9 +56,7 @@ class AfinityApplication : Application(), Configuration.Provider, SingletonImage
}

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()

override fun newImageLoader(context: PlatformContext): ImageLoader {
return ImageLoader.Builder(context)
Expand Down Expand Up @@ -90,4 +84,4 @@ class AfinityApplication : Application(), Configuration.Provider, SingletonImage
.crossfade(true)
.build()
}
}
}
62 changes: 28 additions & 34 deletions app/src/main/java/com/makd/afinity/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,18 @@ import com.makd.afinity.ui.login.LoginScreen
import com.makd.afinity.ui.theme.AFinityTheme
import com.makd.afinity.ui.theme.ThemeMode
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var preferencesRepository: PreferencesRepository
@Inject lateinit var preferencesRepository: PreferencesRepository

@Inject
lateinit var updateManager: UpdateManager
@Inject lateinit var updateManager: UpdateManager

@Inject
lateinit var offlineModeManager: OfflineModeManager
@Inject lateinit var offlineModeManager: OfflineModeManager

@Inject
lateinit var updateScheduler: UpdateScheduler
@Inject lateinit var updateScheduler: UpdateScheduler

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
Expand All @@ -59,25 +55,24 @@ class MainActivity : ComponentActivity() {
setContent {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
val windowSize = calculateWindowSizeClass(this)
val themeMode by preferencesRepository.getThemeModeFlow()
.collectAsState(initial = "SYSTEM")
val dynamicColors by preferencesRepository.getDynamicColorsFlow()
.collectAsState(initial = true)

AFinityTheme(
themeMode = themeMode,
dynamicColor = dynamicColors
) {
val themeMode by
preferencesRepository.getThemeModeFlow().collectAsState(initial = "SYSTEM")
val dynamicColors by
preferencesRepository.getDynamicColorsFlow().collectAsState(initial = true)

AFinityTheme(themeMode = themeMode, dynamicColor = dynamicColors) {
val windowInsetsController =
WindowCompat.getInsetsController(window, window.decorView)
val mode = ThemeMode.fromString(themeMode)
val systemInDarkTheme = isSystemInDarkTheme()

val isLightTheme = when (mode) {
ThemeMode.SYSTEM -> !systemInDarkTheme
ThemeMode.LIGHT -> true
ThemeMode.DARK, ThemeMode.AMOLED -> false
}
val isLightTheme =
when (mode) {
ThemeMode.SYSTEM -> !systemInDarkTheme
ThemeMode.LIGHT -> true
ThemeMode.DARK,
ThemeMode.AMOLED -> false
}

windowInsetsController.isAppearanceLightStatusBars = isLightTheme
windowInsetsController.isAppearanceLightNavigationBars = isLightTheme
Expand All @@ -86,7 +81,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
updateManager = updateManager,
offlineModeManager = offlineModeManager,
widthSizeClass = windowSize.widthSizeClass
widthSizeClass = windowSize.widthSizeClass,
)
}
}
Expand All @@ -105,15 +100,15 @@ private fun MainContent(
viewModel: MainViewModel = hiltViewModel(),
updateManager: UpdateManager,
offlineModeManager: OfflineModeManager,
widthSizeClass: WindowWidthSizeClass
widthSizeClass: WindowWidthSizeClass,
) {
val authState by viewModel.authenticationState.collectAsStateWithLifecycle()

when (authState) {
AuthenticationState.Loading -> {
AfinitySplashScreen(
statusText = stringResource(R.string.splash_status_authenticating),
modifier = modifier
modifier = modifier,
)
}

Expand All @@ -122,19 +117,16 @@ private fun MainContent(
modifier = modifier,
updateManager = updateManager,
offlineModeManager = offlineModeManager,
widthSizeClass = widthSizeClass
widthSizeClass = widthSizeClass,
)
}

AuthenticationState.NotAuthenticated -> {
Scaffold(
modifier = modifier
) { innerPadding ->
Scaffold(modifier = modifier) { innerPadding ->
LoginScreen(
onLoginSuccess = {
},
onLoginSuccess = {},
modifier = Modifier.padding(innerPadding),
widthSizeClass = widthSizeClass
widthSizeClass = widthSizeClass,
)
}
}
Expand All @@ -143,6 +135,8 @@ private fun MainContent(

sealed class AuthenticationState {
object Loading : AuthenticationState()

object Authenticated : AuthenticationState()

object NotAuthenticated : AuthenticationState()
}
}
20 changes: 14 additions & 6 deletions app/src/main/java/com/makd/afinity/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,22 @@ import com.makd.afinity.data.repository.AppDataRepository
import com.makd.afinity.data.repository.auth.AuthRepository
import com.makd.afinity.data.websocket.JellyfinWebSocketManager
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
class MainViewModel
@Inject
constructor(
private val authRepository: AuthRepository,
private val playbackStateManager: PlaybackStateManager,
private val webSocketManager: JellyfinWebSocketManager,
private val appDataRepository: AppDataRepository,
private val offlineModeManager: OfflineModeManager
private val offlineModeManager: OfflineModeManager,
) : ViewModel() {

private val _authenticationState =
Expand Down Expand Up @@ -67,11 +69,17 @@ class MainViewModel @Inject constructor(
private fun observeAuthenticationChanges() {
viewModelScope.launch {
authRepository.isAuthenticated.collect { isAuthenticated ->
if (isAuthenticated && _authenticationState.value != AuthenticationState.Authenticated) {
if (
isAuthenticated &&
_authenticationState.value != AuthenticationState.Authenticated
) {
Timber.d("User authenticated via auth repository")
_authenticationState.value = AuthenticationState.Authenticated
webSocketManager.connect()
} else if (!isAuthenticated && _authenticationState.value == AuthenticationState.Authenticated) {
} else if (
!isAuthenticated &&
_authenticationState.value == AuthenticationState.Authenticated
) {
Timber.d("User logged out via auth repository")
_authenticationState.value = AuthenticationState.NotAuthenticated
webSocketManager.disconnect()
Expand Down Expand Up @@ -99,4 +107,4 @@ class MainViewModel @Inject constructor(
fun refreshAuthState() {
checkAuthenticationState()
}
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/com/makd/afinity/core/AppConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ object AppConstants {

const val CLIENT_ICON_URL =
"https://raw.githubusercontent.com/MakD/AFinity/refs/heads/master/screenshots/Logo/afinity_client_logo.png"
}
}
Loading