Skip to content
Merged
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
44 changes: 44 additions & 0 deletions .github/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Contributing to Aqloss

Thanks for wanting to contribute. Here's what you need to know.

## Project structure

```
lib/ → Flutter frontend (Dart)
rust/src/ → Audio backend (Rust)
audio_engine.rs → playback core
discord_rpc.rs → Discord rich presence
api.rs → bridge between Rust and Flutter
```

## Getting started

1. Make sure you have Flutter and Rust installed
2. Fork the repo and clone your fork
3. Run `flutter pub get` in the root
4. Run `cargo build` inside `rust/`
5. Start the app with `flutter run`

## Before submitting a PR

- Keep changes focused — one fix or feature per PR
- If you're fixing a bug, mention the issue number (`Fixes #123`)
- Test on your platform at minimum; note if you can't test others
- For Rust changes, make sure there are no new `unwrap()`/`expect()` on hot paths — use `?` or `global_opt()` instead
- For Flutter changes, test both desktop and mobile behavior if relevant (they can differ, e.g. context menus)
- Short commit messages are fine, just be descriptive enough

## Code style

- Rust: `cargo fmt` before committing
- Dart: `dart format .` before committing
- Comments are welcome but keep them brief — `// ...` style, not paragraph essays

## What to work on

Check issues labeled `good first issue` or `help wanted`. If you want to work on something, leave a comment first so we don't duplicate effort.

## Reporting bugs

Use the bug report template. Logs are really helpful — especially backend logs and `RUST_BACKTRACE=1` output for panics.
30 changes: 30 additions & 0 deletions .github/issue_template/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: Bug Report
about: Something is broken or not working as expected
labels: bug, needs reproduction
---

## What happened

<!-- Describe the bug clearly. What did you expect vs what actually happened? -->

## Steps to reproduce

1.
2.
3.

## Logs

<!-- Paste relevant backend/frontend logs here. Run with RUST_BACKTRACE=1 if there's a panic. -->

```
(paste logs here)
```

## Environment

- **OS:** <!-- e.g. Arch Linux, Windows 11, macOS 14 -->
- **Aqloss version:** <!-- e.g. v0.3.1 or commit hash -->
- **Audio backend:** <!-- e.g. ALSA, WASAPI, CoreAudio -->
- **File format (if audio-related):** <!-- e.g. FLAC 96kHz/24bit -->
17 changes: 17 additions & 0 deletions .github/issue_template/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: Feature Request
about: Suggest a new feature or improvement
labels: feature
---

## What do you want?

<!-- Describe the feature clearly. What problem does it solve? -->

## Why is this useful?

<!-- Who benefits from this and in what scenario? -->

## Any ideas on implementation?

<!-- Optional. If you have thoughts on how this could work, share them. -->
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ A music player built around a Rust audio engine, with optional WASAPI Exclusive
[![Release](https://img.shields.io/github/v/release/nokarin-dev/aqloss?style=for-the-badge&color=4F8EF7)](https://github.com/nokarin-dev/aqloss/releases/latest)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg?style=for-the-badge)](LICENSE)
[![Flutter](https://img.shields.io/badge/Flutter-3.41-02569B?style=for-the-badge&logo=flutter)](https://flutter.dev)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-02569B?style=for-the-badge)](#download)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20Android-02569B?style=for-the-badge)](#download)

[![Total Downloads](https://img.shields.io/github/downloads/nokarin-dev/aqloss/total?style=for-the-badge&logoColor=%3D&color=3471eb)](https://github.com/nokarin-dev/aqloss/releases)
[![Flathub Downloads](https://img.shields.io/flathub/downloads/xyz.nokarin.aqloss?style=for-the-badge&label=flathub%40installs&color=0451b8)](https://github.com/nokarin-dev/aqloss/releases/latest)
[![Latest Downloads](https://img.shields.io/github/downloads/nokarin-dev/aqloss/latest/total?style=for-the-badge&color=3d47d4)](https://github.com/nokarin-dev/aqloss/releases/latest)
[![Test Status](https://img.shields.io/github/actions/workflow/status/nokarin-dev/aqloss/build-test.yml?style=for-the-badge&label=test%20build&color=22316e)](https://github.com/nokarin-dev/aqloss/actions/workflows/build-test.yml)

Expand Down
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<application
android:label="aqloss"
android:name="${applicationName}"
Expand Down
17 changes: 16 additions & 1 deletion android/app/src/main/kotlin/xyz/nokarin/aqloss/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
package xyz.nokarin.aqloss

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
initAudioContext(applicationContext)
}

private external fun initAudioContext(context: Any)

companion object {
init {
System.loadLibrary("aqloss_rust_core")
}
}
}
2 changes: 2 additions & 0 deletions android/app/src/profile/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
</manifest>
13 changes: 13 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,25 @@ This project loosely follows Keep a Changelog and uses Semantic Versioning.

## [Unreleased]

### Added

- find button discord RPC now links to YouTube Music search
- [Android] Storage permissions handler
- [Android] URI path resolution
- [Android] Folder manager access on mobile

### Fixed

- Call backend only on drag end to prevent seek throttle
- All button now should has pointer now
- Discord button label overflow
- Added helpers to prevent backend crash
- [Android] Library scan empty
- [Android] Status bar overlap
- [Android] window_manager crash on Android
- [Android] Spectrum negative padding
- [Android] Using ndk context to open audio output for cpal
- [Android] Overflow on grid item

---

Expand Down
29 changes: 18 additions & 11 deletions lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:flutter/material.dart' hide ThemeMode;
import 'package:flutter/material.dart' as theme;
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand All @@ -6,6 +7,9 @@ import 'package:aqloss/widgets/settings_watcher.dart';
import 'package:window_manager/window_manager.dart';
import 'screens/home_screen.dart';

bool get _isDesktop =>
Platform.isWindows || Platform.isLinux || Platform.isMacOS;

class AqlossApp extends ConsumerStatefulWidget {
const AqlossApp({super.key});

Expand All @@ -19,19 +23,24 @@ class _AqlossAppState extends ConsumerState<AqlossApp> with WindowListener {
@override
void initState() {
super.initState();
windowManager.addListener(this);
_checkMaximize();
if (_isDesktop) {
windowManager.addListener(this);
_checkMaximize();
}
}

@override
void dispose() {
windowManager.removeListener(this);
if (_isDesktop) windowManager.removeListener(this);
super.dispose();
}

Future<void> _checkMaximize() async {
final fs = await windowManager.isMaximized();
if (mounted) setState(() => _isMaximize = fs);
if (!_isDesktop) return;
try {
final fs = await windowManager.isMaximized();
if (mounted) setState(() => _isMaximize = fs);
} catch (_) {}
}

@override
Expand All @@ -57,12 +66,10 @@ class _AqlossAppState extends ConsumerState<AqlossApp> with WindowListener {
theme: _buildLightTheme(),
darkTheme: _buildDarkTheme(),
builder: (context, child) {
return ClipRRect(
borderRadius: _isMaximize
? BorderRadius.zero
: BorderRadius.circular(12.0),
child: child,
);
final radius = _isDesktop && !_isMaximize
? BorderRadius.circular(12.0)
: BorderRadius.zero;
return ClipRRect(borderRadius: radius, child: child);
},
home: const SettingsWatcher(child: HomeScreen()),
);
Expand Down
25 changes: 16 additions & 9 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,22 @@ void main() async {
runApp(const ProviderScope(child: AqlossApp()));

WidgetsBinding.instance.addPostFrameCallback((_) async {
final (deviceId, exclusive, volume) = await _loadStartupPrefs();
final prefs = await SharedPreferences.getInstance();
final settings = await _loadSettingsState(prefs);
await AudioService.init(
deviceId: deviceId,
exclusive: exclusive,
volume: volume,
settings: settings,
);
if (Platform.isAndroid) {
await Future.delayed(const Duration(milliseconds: 800));
}
try {
final (deviceId, exclusive, volume) = await _loadStartupPrefs();
final prefs = await SharedPreferences.getInstance();
final settings = await _loadSettingsState(prefs);
await AudioService.init(
deviceId: deviceId,
exclusive: exclusive,
volume: volume,
settings: settings,
);
} catch (e, st) {
print('[aqloss] main init error: $e\n$st');
}
});
}

Expand Down
85 changes: 54 additions & 31 deletions lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import 'package:file_picker/file_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:aqloss/util/android_path_helper.dart';
import 'library_screen.dart';
import 'player_screen.dart';
import 'settings_screen.dart';
Expand Down Expand Up @@ -166,39 +167,44 @@ class _HomeScreenState extends ConsumerState<HomeScreen> with WindowListener {
autofocus: true,
onKeyEvent: _handleKey,
child: Scaffold(
body: ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (_isDesktop) _CustomTitleBar(isMaximized: _isMaximized),
Expanded(
child: isWide
? Row(
children: [
_SideNav(
route: _route,
collapsed: _sidebarCollapsed,
onSelect: (r) => setState(() => _route = r),
onToggleCollapse: _toggleSidebar,
),
Expanded(
child: Column(
children: [
Expanded(child: _buildScreen()),
if (hasTrack && _route != 0)
MiniPlayerBar(
onTap: () => setState(() => _route = 0),
),
],
body: SafeArea(
top: !_isDesktop,
bottom: false,
child: ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (_isDesktop) _CustomTitleBar(isMaximized: _isMaximized),
Expanded(
child: isWide
? Row(
children: [
_SideNav(
route: _route,
collapsed: _sidebarCollapsed,
onSelect: (r) => setState(() => _route = r),
onToggleCollapse: _toggleSidebar,
),
),
],
)
: _buildScreen(),
),
],
Expanded(
child: Column(
children: [
Expanded(child: _buildScreen()),
if (hasTrack && _route != 0)
MiniPlayerBar(
onTap: () => setState(() => _route = 0),
),
],
),
),
],
)
: _buildScreen(),
),
],
),
),
),
// SafeArea
bottomNavigationBar: isWide
? null
: _MobileNavBar(
Expand Down Expand Up @@ -1174,11 +1180,28 @@ class _FolderManagerDialog extends ConsumerWidget {
const _FolderManagerDialog();

Future<void> _addFolder(BuildContext context, WidgetRef ref) async {
if (Platform.isAndroid) {
final granted = await requestAndroidStoragePermission();
if (!granted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Storage permission is required to scan music folders',
),
),
);
}
return;
}
}

final result = await FilePicker.getDirectoryPath(
dialogTitle: 'Select music folder',
);
if (result != null) {
ref.read(libraryProvider.notifier).addFolder(result);
final path = resolveAndroidPath(result);
ref.read(libraryProvider.notifier).addFolder(path);
}
}

Expand Down
Loading
Loading