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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ dist/
*.dmg
# Snap package
*.snap
# Snap debugging
squashfs-root/
1 change: 1 addition & 0 deletions assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"serialCopied": "Device serial copied to clipboard"
},
"settings": {
"snapEnvironmentHint": "Executable paths are preset when running in snap",
"adbExecutable": {
"title": "adb executable",
"dialogTitle": "adb scrcpy executable",
Expand Down
27 changes: 3 additions & 24 deletions lib/application/objectbox.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:scrcpy_buddy/service/snap_environment.dart';

import '../objectbox.g.dart'; // created by `flutter pub run build_runner build`
import 'model/profile.dart';
Expand All @@ -14,11 +13,9 @@ class ObjectBox {
// Add any additional setup code, e.g. build queries.
}

static Future<ObjectBox> create() async {
// Get snap-safe directory
final String dbPath = await _getStoragePath();
static Future<ObjectBox> create(SnapEnvironment snapEnvironment) async {
final String dbPath = await snapEnvironment.getStoragePath();

// Ensure directory exists
final dir = Directory(dbPath);
if (!dir.existsSync()) {
await dir.create(recursive: true);
Expand All @@ -29,22 +26,4 @@ class ObjectBox {
}

Box<Profile> get profileBox => store.box();

static Future<String> _getStoragePath() async {
// Check if running in snap
final snapUserCommon = Platform.environment['SNAP_USER_COMMON'];
final snapUserData = Platform.environment['SNAP_USER_DATA'];

if (snapUserCommon != null) {
// Use SNAP_USER_COMMON for persistent data across snap revisions
return path.join(snapUserCommon, 'scrcpy_buddy');
} else if (snapUserData != null) {
// Use SNAP_USER_DATA (backed up on snap refresh)
return path.join(snapUserData, 'scrcpy_buddy');
} else {
// Not running as snap, use default location
final dir = await getApplicationSupportDirectory();
return path.join(dir.path, "scrcpy_buddy_db");
}
}
}
7 changes: 6 additions & 1 deletion lib/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart' as flutter_acrylic;
import 'package:scrcpy_buddy/main.reflectable.dart';
import 'package:scrcpy_buddy/service/snap_environment.dart';
import 'package:system_theme/system_theme.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
Expand Down Expand Up @@ -39,7 +40,11 @@ Future<void> initTrayIcon([Brightness platformBrightness = Brightness.light]) as
// tray icon init
try {
final iconName = platformBrightness == Brightness.dark ? 'icon_light' : 'icon_dark';
await trayManager.setIcon(Platform.isWindows ? 'assets/tray/$iconName.ico' : 'assets/tray/$iconName.png');
final snapIconPath = SnapEnvironment().getSnapTrayIconPath(iconName);
final iconPath =
snapIconPath ??
(Platform.isWindows ? 'assets/tray/$iconName.ico' : 'assets/tray/$iconName.png'); // fallback for non-snap
await trayManager.setIcon(iconPath);
if (Platform.isLinux) {
// Don't show title on macOS (takes up more space in menu bar)
// Not supported on Windows
Expand Down
7 changes: 6 additions & 1 deletion lib/injector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import 'package:scrcpy_buddy/service/adb_service.dart';
import 'package:scrcpy_buddy/service/running_process_manager.dart';
import 'package:scrcpy_buddy/service/scrcpy_service.dart';
import 'package:scrcpy_buddy/service/snap_environment.dart';

import 'application/adb_result_parser.dart';

Expand All @@ -16,7 +17,11 @@ List<Provider> get providers => [
create: (context) => AdbService(context.read<ProcessManager>(), context.read<AdbResultParser>()),
),

Provider<ScrcpyService>(create: (context) => ScrcpyService(context.read<ProcessManager>())),
Provider<SnapEnvironment>(create: (_) => SnapEnvironment()),

Provider<ScrcpyService>(
create: (context) => ScrcpyService(context.read<ProcessManager>(), context.read<SnapEnvironment>()),
),

Provider<RunningProcessManager>(create: (_) => RunningProcessManager()),
];
3 changes: 2 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:scrcpy_buddy/init.dart';
import 'package:scrcpy_buddy/presentation/devices/bloc/devices_bloc.dart';
import 'package:scrcpy_buddy/presentation/search/bloc/search_bloc.dart';
import 'package:scrcpy_buddy/routes.dart';
import 'package:scrcpy_buddy/service/snap_environment.dart';
import 'package:system_theme/system_theme.dart';
import 'package:window_manager/window_manager.dart';

Expand All @@ -30,7 +31,7 @@ late ObjectBox _objectBox;
void main() async {
await init();
await _prefs.initialize();
_objectBox = await ObjectBox.create();
_objectBox = await ObjectBox.create(SnapEnvironment());
runApp(const MyApp());
}

Expand Down
20 changes: 13 additions & 7 deletions lib/presentation/settings/widget/adb_executable_setting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:scrcpy_buddy/presentation/devices/bloc/devices_bloc.dart';
import 'package:scrcpy_buddy/presentation/extension/translation_extension.dart';
import 'package:scrcpy_buddy/presentation/widgets/app_widgets.dart';
import 'package:scrcpy_buddy/service/adb_service.dart';
import 'package:scrcpy_buddy/service/snap_environment.dart';

class AdbExecutableSetting extends StatefulWidget {
const AdbExecutableSetting({super.key});
Expand All @@ -25,6 +26,7 @@ class _AdbExecutableSettingState extends AppModuleState<AdbExecutableSetting> {
late final _executablePreference = context.read<AppSettings>().adbExecutable;
late final _adbService = context.read<AdbService>();
late final _devicesBloc = context.read<DevicesBloc>();
late final _snapEnvironment = context.read<SnapEnvironment>();

final _infoFlyoutController = FlyoutController();
final _textController = TextEditingController();
Expand Down Expand Up @@ -96,26 +98,30 @@ class _AdbExecutableSettingState extends AppModuleState<AdbExecutableSetting> {

@override
Widget build(BuildContext context) {
final isSnap = _snapEnvironment.isRunningInSnap;
return Row(
children: [
Expanded(
child: TextBox(
controller: _textController,
enabled: !_isSaving,
enabled: !_isSaving && !isSnap,
textInputAction: TextInputAction.done,
placeholder: isSnap ? context.translatedText(key: 'settings.snapEnvironmentHint') : null,
onSubmitted: (value) => _validateAndSave(value),
onTapOutside: (_) => _validateAndSave(_textController.text),
suffix: _isSaving
? ProgressRing()
: IconButton(
icon: Icon(WindowsIcons.file_explorer),
onPressed: () async {
final result = await openFile();
onPressed: isSnap
? null
: () async {
final result = await openFile();

if (result != null) {
_validateAndSave(result.path);
}
},
if (result != null) {
_validateAndSave(result.path);
}
},
),
),
),
Expand Down
20 changes: 13 additions & 7 deletions lib/presentation/settings/widget/scrcpy_executable_setting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:scrcpy_buddy/application/extension/scrcpy_error_extension.dart';
import 'package:scrcpy_buddy/presentation/extension/translation_extension.dart';
import 'package:scrcpy_buddy/presentation/widgets/app_widgets.dart';
import 'package:scrcpy_buddy/service/scrcpy_service.dart';
import 'package:scrcpy_buddy/service/snap_environment.dart';

class ScrcpyExecutableSetting extends StatefulWidget {
const ScrcpyExecutableSetting({super.key});
Expand All @@ -22,6 +23,7 @@ class _ScrcpyExecutableSettingState extends AppModuleState<ScrcpyExecutableSetti

late final _executablePreference = context.read<AppSettings>().scrcpyExecutable;
late final _scrcpyService = context.read<ScrcpyService>();
late final _snapEnvironment = context.read<SnapEnvironment>();

final _infoFlyoutController = FlyoutController();
final _textController = TextEditingController();
Expand Down Expand Up @@ -85,26 +87,30 @@ class _ScrcpyExecutableSettingState extends AppModuleState<ScrcpyExecutableSetti

@override
Widget build(BuildContext context) {
final isSnap = _snapEnvironment.isRunningInSnap;
return Row(
children: [
Expanded(
child: TextBox(
controller: _textController,
enabled: !_isSaving,
enabled: !_isSaving && !isSnap,
textInputAction: TextInputAction.done,
placeholder: isSnap ? context.translatedText(key: 'settings.snapEnvironmentHint') : null,
onSubmitted: (value) => _validateAndSave(value),
onTapOutside: (_) => _validateAndSave(_textController.text),
suffix: _isSaving
? ProgressRing()
: IconButton(
icon: Icon(WindowsIcons.file_explorer),
onPressed: () async {
final result = await openFile();
onPressed: isSnap
? null
: () async {
final result = await openFile();

if (result != null) {
_validateAndSave(result.path);
}
},
if (result != null) {
_validateAndSave(result.path);
}
},
),
),
),
Expand Down
10 changes: 8 additions & 2 deletions lib/service/scrcpy_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import 'package:process/process.dart';
import 'package:scrcpy_buddy/application/model/scrcpy/device_app.dart';
import 'package:scrcpy_buddy/application/model/scrcpy/scrcpy_error.dart';
import 'package:scrcpy_buddy/application/model/scrcpy/scrcpy_result.dart';
import 'package:scrcpy_buddy/service/snap_environment.dart';

class ScrcpyService {
final ProcessManager _processManager;
final SnapEnvironment _snapEnvironment;

ScrcpyService(this._processManager);
ScrcpyService(this._processManager, this._snapEnvironment);

Future<ScrcpyResult> start(String deviceSerial, String adbPath, String path, List<String> args) async {
try {
Expand Down Expand Up @@ -90,5 +92,9 @@ class ScrcpyService {

Map<String, String>? _getEnvironment(String adbPath) => adbPath.isNotEmpty ? {'ADB': adbPath} : null;

String _getExecutable(String path) => path.isEmpty ? 'scrcpy' : path;
String _getExecutable(String path) => path.isEmpty ? getScrcpyPath() : path;

/// Returns the resolved scrcpy binary path, preferring the Snap bundle
/// when running inside a Snap package.
String getScrcpyPath() => _snapEnvironment.getSnapScrcpyPath() ?? 'scrcpy';
}
48 changes: 48 additions & 0 deletions lib/service/snap_environment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'dart:developer' as developer;
import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';

/// Provides information about the Snap environment the app may be running in.
class SnapEnvironment {
/// Returns `true` if the application is running inside a Snap package.
bool get isRunningInSnap => Platform.environment['SNAP'] != null;

/// Returns the correct persistent storage directory for the app,
/// respecting the Snap environment variables when running as a Snap.
///
/// Priority: `SNAP_USER_COMMON` → `SNAP_USER_DATA` → system app support dir.
Future<String> getStoragePath() async {
final snapUserCommon = Platform.environment['SNAP_USER_COMMON'];
final snapUserData = Platform.environment['SNAP_USER_DATA'];

if (snapUserCommon != null) {
return path.join(snapUserCommon, 'scrcpy_buddy');
} else if (snapUserData != null) {
return path.join(snapUserData, 'scrcpy_buddy');
} else {
final dir = await getApplicationSupportDirectory();
return path.join(dir.path, 'scrcpy_buddy_db');
}
}

/// Returns the scrcpy binary path for the Snap bundle, or `null` if the
/// bundle path cannot be determined or the binary does not exist there.
String? getSnapScrcpyPath() {
final snapPath = Platform.environment['SNAP'];
if (snapPath == null) return null;

developer.log('SNAP_PATH: $snapPath', name: 'scrcpy_buddy.snap');
final scrcpyPath = '$snapPath/scrcpy-runtime/usr/local/bin/scrcpy';
return File(scrcpyPath).existsSync() ? scrcpyPath : null;
}

/// Returns the path to the tray icon in the Snap bundle, or `null` if the
/// bundle path cannot be determined.
String? getSnapTrayIconPath(String iconName) {
final snapDir = Platform.environment['SNAP'];
if (snapDir == null) return null;
return '$snapDir/data/flutter_assets/assets/tray/$iconName.png';
}
}
40 changes: 40 additions & 0 deletions scripts/rebuild_snap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail

SNAP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SNAP_NAME="scrcpy-buddy"

echo "==> Working directory: $SNAP_DIR"
cd "$SNAP_DIR"

# 1. Remove installed snaps
echo ""
echo "==> [1/5] Removing installed snaps..."
sudo snap remove --purge "$SNAP_NAME" || true
sudo snap remove --purge scrcpy-runtime-2404 || true

# 2. Clean snapcraft build artifacts
echo ""
echo "==> [2/5] Cleaning snapcraft build..."
snapcraft clean --use-lxd

# 3. Build the snap
echo ""
echo "==> [3/5] Building snap with LXD..."
snapcraft pack --use-lxd

# 4. Install the freshly built snap
SNAP_FILE=$(ls -t "${SNAP_DIR}"/*.snap | head -1)
echo ""
echo "==> [4/5] Installing snap: $SNAP_FILE"
sudo snap install --dangerous "$SNAP_FILE"

# 5. Connect interfaces
echo ""
echo "==> [5/5] Connecting snap interfaces..."
sudo snap disconnect "$SNAP_NAME:gnome-46-2404" || true
sudo snap connect "$SNAP_NAME:gnome-46-2404" gnome-46-2404:gnome-46-2404
sudo snap connect "$SNAP_NAME:scrcpy-runtime-2404" scrcpy-runtime-2404:scrcpy-runtime-2404

echo ""
echo "==> Done! You can now launch: snap run $SNAP_NAME"
2 changes: 1 addition & 1 deletion snap/gui/scrcpy-buddy.desktop
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Name=scrcpy buddy
Comment=GUI wrapper for scrcpy
Exec=scrcpy-buddy
Icon=${SNAP}/meta/gui/icon_light.png
Icon=${SNAP}/meta/gui/icon.png
Terminal=false
Type=Application
Categories=Utility;
Expand Down
Loading
Loading