Restructure step R: the app is Starsync (dev.foxyz.starsync)
ci / core (push) Successful in 31s
ci / server (push) Successful in 1m3s
ci / updater-windows (push) Successful in 6m6s

- Linux app id dev.foxyz.starsync, binary starsync; Windows starsync.exe,
  company foxyz; server starsync-server; Dart package starsync; archives
  starsync-<ver>-*.
- 1.2.x compatibility: watch_together and watch-together-server
  launchers (Linux) / copies (Windows) in the archive; the updater
  accepts both names and relaunches the new binary; update.sh/.bat use
  whichever server exists.
- First start copies (never moves) the old support folder and this
  app's Hive boxes out of ~/Documents into <support>/hive; a marker
  makes it run once; AppDirs is a port; settings schema version with
  ordered migrations.
- Launcher entry dev.foxyz.starsync.desktop; the old one removed.
- Repository name and URLs unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-15 20:21:20 +03:00
1 parent f4ca02c3cf
commit 51c1c5ab78
96 files changed
+1169 -489

No files matched your search

+3 -4
View File
@@ -7,9 +7,9 @@ import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'app.dart';
import 'src/platform/app_dirs.dart';
import 'src/platform/app_log.dart';
import 'src/platform/app_shutdown.dart';
import 'src/platform/desktop/desktop_app_dirs.dart';
import 'src/platform/desktop_entry.dart';
import 'src/screens/home/ui.dart';
import 'src/session/app_events.dart' show UpdateAvailable;
@@ -42,10 +42,9 @@ Future<void> main(List<String> args) async {
),
);
WidgetsFlutterBinding.ensureInitialized();
final dirs = await AppDirs.resolve();
final dirs = await const DesktopAppDirs().resolve();
await startAppLogging(dirs);
// An absolute subDir replaces the documents folder (package:path join).
await Hive.initFlutter(dirs.hive);
Hive.init(dirs.hive);
await AppSettings.instance.ensureLoaded();
// Close button / Alt+F4: the orderly close ([shutDownApp]), so others
// see "left", a hosted server shuts down and no player outlives the
@@ -1,64 +0,0 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
/// The environment variable that picks a separate profile (DEVELOPMENT.md
/// "Dev loop": two instances on one machine).
const profileEnv = 'STARSYNC_PROFILE';
/// Where the app keeps its own files, resolved once in `main` ([resolve])
/// and handed on from there.
///
/// Without a profile it is exactly today's layout: Hive's boxes in Hive's
/// default folder (the documents folder) and the logs in
/// `<app support>/logs`. With `STARSYNC_PROFILE=<name>` (letters, digits,
/// `-`, `_`; at most 32) the per-install data moves to
/// `<app support>/profiles/<name>/`: the Hive boxes (settings and so the
/// profile, connect form, window bounds, folders, Jellyfin sign-ins; the
/// client id from restructure step 1 on) under `hive/`, the logs under
/// `logs/`. Downloads and caches stay shared: yt-dlp and its cookies, the
/// extracted shaders, thumbnails, the AniList cache.
class AppDirs {
/// The profile name; null for the normal app.
final String? profile;
/// The app support folder (yt-dlp, shaders; logs without a profile).
final String support;
/// Hive's folder; null: Hive's default (`Hive.initFlutter()`).
final String? hive;
/// The app's log folder.
final String logs;
const AppDirs({required this.profile, required this.support, required this.hive, required this.logs});
/// The layout for [support] (the app support folder) and [env]; an
/// invalid profile name is ignored ([invalidProfile] says which).
static AppDirs fromEnv({required String support, required Map<String, String> env}) {
final sep = Platform.pathSeparator;
final name = validProfile(env[profileEnv]);
if (name == null) return AppDirs(profile: null, support: support, hive: null, logs: '$support${sep}logs');
final root = '$support${sep}profiles$sep$name';
return AppDirs(profile: name, support: support, hive: '$root${sep}hive', logs: '$root${sep}logs');
}
/// [raw] when it is a usable profile name, else null.
static String? validProfile(String? raw) {
final v = raw?.trim() ?? '';
return RegExp(r'^[A-Za-z0-9_-]{1,32}$').hasMatch(v) ? v : null;
}
/// The env value when it is set but not a usable name (for a warning).
static String? invalidProfile(Map<String, String> env) {
final raw = env[profileEnv];
return raw != null && raw.trim().isNotEmpty && validProfile(raw) == null ? raw : null;
}
/// Resolves the layout from the platform and the process environment.
/// Needs Flutter's binding (path_provider).
static Future<AppDirs> resolve() async {
final support = (await getApplicationSupportDirectory()).path;
final dirs = fromEnv(support: support, env: Platform.environment);
final hive = dirs.hive;
if (hive != null) await Directory(hive).create(recursive: true);
return dirs;
}
}
+4 -1
View File
@@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:sync_relay_core/core.dart';
import '../screens/home/ui.dart' show appName, displayVersion;
import 'app_dirs.dart';
import 'ports/app_dirs.dart';
import '../state/session_state.dart';
// The app's log on disk (<app support>/logs/app-YYYY-MM-DD.log; core's
@@ -39,6 +39,9 @@ Future<void> startAppLogging(AppDirs dirs) async {
_appLog.info('$appName ${displayVersion()} on ${Platform.operatingSystem} '
'${Platform.operatingSystemVersion}, pid $pid, Dart ${Platform.version.split(' ').first}'
'${dirs.profile == null ? '' : ', profile ${dirs.profile}'}');
for (final line in dirs.migrated) {
_appLog.info(line);
}
final bad = AppDirs.invalidProfile(Platform.environment);
if (bad != null) _appLog.warning('$profileEnv="$bad" ignored: use letters, digits, - and _ (at most 32)');
}
@@ -0,0 +1,116 @@
import 'dart:convert';
import 'dart:io';
// First start of the renamed app (Starsync, app id dev.foxyz.starsync):
// copy what the old names left, never move, so a 1.2.x install keeps
// working next to it. Runs once (the marker file), then never again.
/// Written into the new support folder once the migration ran (whether or
/// not anything was found): `migratedFrom` (the old support folder or
/// null), `hiveFrom` (the folder the boxes came from or null), `boxes`,
/// `files`.
const migrationMarker = 'migration.json';
/// The Hive boxes the app opens, by name (settings, connect form, Files,
/// Jellyfin sign-ins, track choices). Only these leave the documents
/// folder, which other apps' boxes share.
const appHiveBoxes = ['settings', 'connect_form', 'files', 'jellyfin', 'track_prefs'];
/// Support folders of the old names for the new [support] folder, the
/// most likely first. Linux: `$XDG_DATA_HOME/moe.senkochan.watch_together`
/// (the GTK application id up to 1.2.x), then `watch_together` (the
/// executable name path_provider falls back to). Windows:
/// `%APPDATA%\com.example\watch_together` (company and product name of
/// the exe up to 1.2.x).
List<String> legacySupportDirs(String support, {bool? windows}) {
final win = windows ?? Platform.isWindows;
final sep = win ? r'\' : '/';
String parent(String p) {
final norm = p.endsWith(sep) ? p.substring(0, p.length - 1) : p;
final i = norm.lastIndexOf(sep);
return i <= 0 ? norm : norm.substring(0, i);
}
if (win) return ['${parent(parent(support))}${sep}com.example${sep}watch_together'];
final share = parent(support);
return ['$share${sep}moe.senkochan.watch_together', '$share${sep}watch_together'];
}
/// Copies the first existing [oldSupport] folder into [support] and the
/// [appHiveBoxes] (`.hive` and `.lock`) from [documents] into [hive],
/// skipping anything already there; the originals stay. Once per support
/// folder ([migrationMarker]). Returns one log line per thing copied
/// (paths and counts, never contents); empty when it ran before.
Future<List<String>> migrateLegacyData(
{required String support, required String hive, required List<String> oldSupport, String? documents}) async {
final sep = Platform.pathSeparator;
final marker = File('$support$sep$migrationMarker');
if (marker.existsSync()) return const [];
final lines = <String>[];
String? from;
var files = 0;
for (final old in oldSupport) {
final dir = Directory(old);
if (!dir.existsSync() || _same(old, support)) continue;
files = await _copyTree(dir, Directory(support));
from = old;
lines.add('migration: copied $files files from $old to $support');
break;
}
final boxes = <String>[];
if (documents != null && Directory(documents).existsSync() && !_same(documents, hive)) {
await Directory(hive).create(recursive: true);
for (final box in appHiveBoxes) {
for (final ext in const ['hive', 'lock']) {
final src = File('$documents$sep$box.$ext');
final dst = File('$hive$sep$box.$ext');
if (!src.existsSync() || dst.existsSync()) continue;
await src.copy(dst.path);
if (ext == 'hive') boxes.add(box);
}
}
if (boxes.isNotEmpty) {
lines.add('migration: copied ${boxes.length} Hive boxes (${boxes.join(', ')}) from $documents to $hive');
}
}
if (lines.isEmpty) lines.add('migration: no data from before the rename');
await Directory(support).create(recursive: true);
await marker.writeAsString(jsonEncode({
'migratedFrom': from,
'hiveFrom': boxes.isEmpty ? null : documents,
'boxes': boxes,
'files': files,
}));
return lines;
}
bool _same(String a, String b) {
try {
return FileSystemEntity.identicalSync(a, b);
} catch (_) {
return false;
}
}
// Files (File.copy keeps the mode on POSIX: cookies.txt stays 0600, yt-dlp
// executable) and symlinks as they are; existing targets win.
Future<int> _copyTree(Directory from, Directory to) async {
var n = 0;
await to.create(recursive: true);
await for (final e in from.list(recursive: true, followLinks: false)) {
final target = '${to.path}${e.path.substring(from.path.length)}';
if (e is Directory) {
await Directory(target).create(recursive: true);
} else if (await FileSystemEntity.type(target, followLinks: false) != FileSystemEntityType.notFound) {
continue;
} else if (e is Link) {
await Directory(File(target).parent.path).create(recursive: true);
await Link(target).create(await e.target());
} else if (e is File) {
await Directory(File(target).parent.path).create(recursive: true);
await e.copy(target);
n++;
}
}
return n;
}
@@ -0,0 +1,35 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../data_migration.dart';
import '../ports/app_dirs.dart';
/// The desktop's [AppDirsPort]: the app support folder from path_provider
/// (Linux `$XDG_DATA_HOME/dev.foxyz.starsync`, Windows
/// `%APPDATA%\foxyz\Starsync`); on the first start it copies the data of
/// the old names in ([migrateLegacyData]). Needs Flutter's binding.
class DesktopAppDirs implements AppDirsPort {
const DesktopAppDirs();
@override
Future<AppDirs> resolve() async {
final support = (await getApplicationSupportDirectory()).path;
final base = AppDirs.fromEnv(support: support, env: const {});
List<String> migrated;
try {
String? documents;
try {
// Where Hive.initFlutter() kept the boxes up to 1.2.x.
documents = (await getApplicationDocumentsDirectory()).path;
} catch (_) {}
migrated = await migrateLegacyData(
support: support, hive: base.hive, oldSupport: legacySupportDirs(support), documents: documents);
} catch (e) {
migrated = ['migration failed: $e'];
}
final dirs = AppDirs.fromEnv(support: support, env: Platform.environment, migrated: migrated);
await Directory(dirs.hive).create(recursive: true);
return dirs;
}
}
@@ -5,16 +5,24 @@ import 'package:sync_relay_core/core.dart' show findInstallRoot;
/// The app's id: GTK's application id, so the Wayland `app_id` the
/// compositor matches to [desktopFileName] for the window's icon.
const appId = 'moe.senkochan.watch_together';
const appId = 'dev.foxyz.starsync';
const desktopFileName = '$appId.desktop';
/// The app id up to 1.2.x: its entry and icons are removed (only those
/// exact files, which that version wrote).
const legacyAppId = 'moe.senkochan.watch_together';
const _iconSizes = [16, 32, 48, 64, 128, 256, 512];
/// Linux release installs (an unpacked tarball) carry no launcher entry, so
/// Wayland shows a generic icon. On start: write the `.desktop` file into
/// `~/.local/share/applications` (Exec = this binary) and the icon into
/// the hicolor theme, only when something changed (a moved install, a new
/// icon). Never in debug runs; failures only log.
Future<void> installDesktopEntry({String? executable, String? dataHome, String? home}) async {
if (!Platform.isLinux || kDebugMode) return;
/// icon). The entry and icons of [legacyAppId] go. Never in debug runs
/// (unless [force], for tests); failures only log.
Future<void> installDesktopEntry(
{String? executable, String? dataHome, String? home, @visibleForTesting bool force = false}) async {
if (!Platform.isLinux || (kDebugMode && !force)) return;
try {
final exe = executable ?? Platform.resolvedExecutable;
final root = findInstallRoot(exe, windows: false);
@@ -22,7 +30,8 @@ Future<void> installDesktopEntry({String? executable, String? dataHome, String?
final share = dataHome ?? Platform.environment['XDG_DATA_HOME'] ??
'${home ?? Platform.environment['HOME']}/.local/share';
final icons = '${root.path}/data/flutter_assets/assets/icon';
for (final size in const [16, 32, 48, 64, 128, 256, 512]) {
await _removeLegacy(share);
for (final size in _iconSizes) {
final src = File('$icons/icon_$size.png');
if (!src.existsSync()) continue;
final dst = File('$share/icons/hicolor/${size}x$size/apps/$appId.png');
@@ -40,6 +49,21 @@ Future<void> installDesktopEntry({String? executable, String? dataHome, String?
}
}
Future<void> _removeLegacy(String share) async {
for (final f in [
File('$share/applications/$legacyAppId.desktop'),
for (final size in _iconSizes) File('$share/icons/hicolor/${size}x$size/apps/$legacyAppId.png'),
]) {
if (!f.existsSync()) continue;
try {
await f.delete();
debugPrint('desktop entry: removed ${f.path}');
} catch (e) {
debugPrint('desktop entry: cannot remove ${f.path}: $e');
}
}
}
@visibleForTesting
String desktopEntryText(String exe) => '''[Desktop Entry]
Type=Application
@@ -0,0 +1,69 @@
import 'dart:io';
/// The environment variable that picks a separate profile (DEVELOPMENT.md
/// "Dev loop": two instances on one machine).
const profileEnv = 'STARSYNC_PROFILE';
/// Where the app keeps its own files, resolved once in `main` through an
/// [AppDirsPort] and handed on from there.
///
/// Everything lives in the app support folder: Hive's boxes in
/// `<app support>/hive`, the logs in `<app support>/logs`. With
/// `STARSYNC_PROFILE=<name>` (letters, digits, `-`, `_`; at most 32) the
/// per-install data moves to `<app support>/profiles/<name>/`: the Hive
/// boxes (settings and so the profile, connect form, window bounds,
/// folders, Jellyfin sign-ins; the client id from restructure step 1 on)
/// under `hive/`, the logs under `logs/`. Downloads and caches stay
/// shared: yt-dlp and its cookies, the extracted shaders, thumbnails, the
/// AniList cache.
class AppDirs {
/// The profile name; null for the normal app.
final String? profile;
/// The app support folder (yt-dlp, shaders; Hive and logs without a
/// profile).
final String support;
/// Hive's folder.
final String hive;
/// The app's log folder.
final String logs;
/// What resolving copied over from before the rename, one line each
/// (paths and counts), for the log once it is open.
final List<String> migrated;
const AppDirs(
{required this.profile,
required this.support,
required this.hive,
required this.logs,
this.migrated = const []});
/// The layout for [support] (the app support folder) and [env]; an
/// invalid profile name is ignored ([invalidProfile] says which).
static AppDirs fromEnv(
{required String support, required Map<String, String> env, List<String> migrated = const []}) {
final sep = Platform.pathSeparator;
final name = validProfile(env[profileEnv]);
final root = name == null ? support : '$support${sep}profiles$sep$name';
return AppDirs(
profile: name, support: support, hive: '$root${sep}hive', logs: '$root${sep}logs', migrated: migrated);
}
/// [raw] when it is a usable profile name, else null.
static String? validProfile(String? raw) {
final v = raw?.trim() ?? '';
return RegExp(r'^[A-Za-z0-9_-]{1,32}$').hasMatch(v) ? v : null;
}
/// The env value when it is set but not a usable name (for a warning).
static String? invalidProfile(Map<String, String> env) {
final raw = env[profileEnv];
return raw != null && raw.trim().isNotEmpty && validProfile(raw) == null ? raw : null;
}
}
/// Finds the app's folders on this platform (and, the first time, brings
/// over the data of the names before the rename).
abstract interface class AppDirsPort {
/// The layout for this process; its Hive folder exists afterwards.
Future<AppDirs> resolve();
}
@@ -4,6 +4,7 @@ import 'shell_port.dart';
import 'system_notifications.dart';
import 'url_opener.dart';
export 'app_dirs.dart';
export 'folder_access.dart';
export 'link_resolver.dart';
export 'shell_port.dart';
+1 -1
View File
@@ -158,7 +158,7 @@ class AniListClient {
_cacheDir = cacheDir ?? _defaultCacheDir,
_delay = delay ?? Future<void>.delayed,
_now = now ?? DateTime.now,
userAgent = userAgent ?? 'watch-together/$appVersion';
userAgent = userAgent ?? 'Starsync/$appVersion';
final Uri url;
final Duration memoryTtl;
+1 -1
View File
@@ -474,7 +474,7 @@ class AnimeFlowClient {
_ownsHttp = http == null,
_delay = delay ?? Future<void>.delayed,
_now = now ?? DateTime.now,
userAgent = userAgent ?? 'watch-together/$appVersion';
userAgent = userAgent ?? 'Starsync/$appVersion';
/// Authenticated with the linked Jellyfin account on the same domain
/// (see [tokenFor]), looked up on every call so a re-link applies.
+4
View File
@@ -6,6 +6,7 @@ import 'package:signals/signals_flutter.dart';
import '../controls/model/controls_prefs.dart';
import '../shaders/shader_presets.dart';
import 'settings_schema.dart';
// App preferences (Settings page), one Hive box. Signals start at their
// defaults and take the stored values once [AppSettings.ensureLoaded]
@@ -134,6 +135,9 @@ class AppSettings {
Future<void> _loadNow() async {
try {
final box = _box = await Hive.openBox(settingsBoxName);
for (final line in await migrateSettings(box)) {
debugPrint(line);
}
final mode = box.get(_keyHwdecMode);
hwdecMode.value = HwdecMode.values
.where((m) => m.name == mode)
@@ -0,0 +1,46 @@
import 'package:hive_ce/hive.dart';
// The `settings` box's layout version and the ordered steps between
// versions. A step rewrites stored values for a new layout (renamed keys,
// changed formats); the restructure steps add theirs here.
/// The layout this build reads. 1: the layout of 1.2.x, where versioning
/// starts (a box without a version is at 1).
const settingsSchemaVersion = 1;
/// The box key holding the stored version.
const settingsSchemaKey = 'schemaVersion';
/// One step: brings a box at `to - 1` to [to].
class SettingsMigration {
final int to;
/// For the log ("settings schema 2: ...").
final String what;
final Future<void> Function(Box box) apply;
const SettingsMigration(this.to, this.what, this.apply);
}
/// Every step in order of [SettingsMigration.to] (2, 3, ...); empty while
/// version 1 is current.
const settingsMigrations = <SettingsMigration>[];
/// Runs the [steps] past the box's stored version up to [current], in
/// order, storing the version after each, then stores [current]. A box
/// from a newer build (a downgrade) is left as it is. Returns a log line
/// per step run.
Future<List<String>> migrateSettings(Box box,
{List<SettingsMigration> steps = settingsMigrations, int current = settingsSchemaVersion}) async {
final stored = box.get(settingsSchemaKey);
var at = stored is int ? stored : 1;
if (at > current) return ['settings schema $at is newer than this build ($current): left as is'];
final ran = <String>[];
for (final step in [...steps]..sort((a, b) => a.to.compareTo(b.to))) {
if (step.to <= at || step.to > current) continue;
await step.apply(box);
at = step.to;
await box.put(settingsSchemaKey, at);
ran.add('settings schema ${step.to}: ${step.what}');
}
if (stored != current) await box.put(settingsSchemaKey, current);
return ran;
}
@@ -1,4 +1,7 @@
@echo off
rem Update this install in place (same as clicking Update in the app).
"%~dp0watch-together-server.exe" --upgrade %*
rem The old server name is the fallback for installs from before the rename.
set "SERVER=%~dp0starsync-server.exe"
if not exist "%SERVER%" set "SERVER=%~dp0watch-together-server.exe"
"%SERVER%" --upgrade %*
pause
+4 -1
View File
@@ -1,4 +1,7 @@
#!/bin/sh
# Update this install in place (same as clicking Update in the app).
# The server binary runs the updater: it needs no GTK/display (VPS).
cd "$(dirname "$0")" && exec ./watch-together-server --upgrade "$@"
# The old server name is the fallback for installs from before the rename.
cd "$(dirname "$0")" || exit 1
if [ -x ./starsync-server ]; then exec ./starsync-server --upgrade "$@"; fi
exec ./watch-together-server --upgrade "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# The dedicated server's name up to 1.2.x: systemd units (ExecStart) and
# old update.sh copies run it.
exec "$(dirname "$(readlink -f "$0")")/starsync-server" "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Starsync's name up to 1.2.x: their updater restarts it after updating to
# this release, and launchers or scripts may still point at it.
exec "$(dirname "$(readlink -f "$0")")/starsync" "$@"
@@ -173,6 +173,8 @@ class UpdateController {
_log.info('updated to ${m.version}, restarting');
status.value =
UpdateStatus(UpdatePhase.restarting, manifest: m, canInstall: true);
await restart(_exe, launchArgs);
// Started by an old name (1.2.x's restart, an old launcher): the new
// binary next to it.
await restart(relaunchExecutable(_exe), launchArgs);
}
}
+2 -2
View File
@@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "watch_together")
set(BINARY_NAME "starsync")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "moe.senkochan.watch_together")
set(APPLICATION_ID "dev.foxyz.starsync")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
+2 -2
View File
@@ -1,5 +1,5 @@
name: watch_together
description: Watch-together desktop app (working title, repo sync_relay_project). One binary.
name: starsync
description: Starsync desktop app (repo sync_relay_project). One binary.
publish_to: none
version: 1.2.0
environment:
+144 -22
View File
@@ -1,36 +1,158 @@
// restructure: rewrite in step R (Rename): Hive moves into the support
// folder for the normal app too.
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/platform/app_dirs.dart';
import 'package:starsync/src/files/folder_store.dart' show filesBoxName;
import 'package:starsync/src/jellyfin/accounts.dart' show jellyfinBoxName;
import 'package:starsync/src/jellyfin/track_prefs.dart' show trackPrefsBoxName;
import 'package:starsync/src/platform/data_migration.dart';
import 'package:starsync/src/platform/ports/app_dirs.dart';
import 'package:starsync/src/state/connect_prefs.dart' show connectFormBoxName;
import 'package:starsync/src/state/settings.dart' show settingsBoxName;
void main() {
final sep = Platform.pathSeparator;
const support = '/s';
test('no profile: today\'s layout', () {
final d = AppDirs.fromEnv(support: support, env: const {});
expect(d.profile, isNull);
expect(d.hive, isNull, reason: "Hive's default folder");
expect(d.logs, '$support${sep}logs');
group('AppDirs', () {
test('no profile: Hive and logs in the support folder', () {
final d = AppDirs.fromEnv(support: support, env: const {});
expect(d.profile, isNull);
expect(d.hive, '$support${sep}hive');
expect(d.logs, '$support${sep}logs');
});
test('a profile gets its own Hive and logs folders', () {
final d = AppDirs.fromEnv(support: support, env: const {profileEnv: 'b'});
expect(d.profile, 'b');
expect(d.hive, '$support${sep}profiles${sep}b${sep}hive');
expect(d.logs, '$support${sep}profiles${sep}b${sep}logs');
expect(d.support, support, reason: 'yt-dlp and shaders stay shared');
});
test('an unusable name is ignored and reported', () {
for (final bad in ['../x', 'a b', 'x' * 33, 'é']) {
final env = {profileEnv: bad};
expect(AppDirs.fromEnv(support: support, env: env).profile, isNull, reason: bad);
expect(AppDirs.invalidProfile(env), bad);
}
expect(AppDirs.invalidProfile(const {profileEnv: ''}), isNull, reason: 'empty is just unset');
expect(AppDirs.invalidProfile(const {profileEnv: 'ok_1-2'}), isNull);
});
});
test('a profile gets its own Hive and logs folders', () {
final d = AppDirs.fromEnv(support: support, env: const {profileEnv: 'b'});
expect(d.profile, 'b');
expect(d.hive, '$support${sep}profiles${sep}b${sep}hive');
expect(d.logs, '$support${sep}profiles${sep}b${sep}logs');
expect(d.support, support, reason: 'yt-dlp and shaders stay shared');
test('the migrated boxes are the ones the app opens', () {
expect(appHiveBoxes.toSet(),
{settingsBoxName, connectFormBoxName, filesBoxName, jellyfinBoxName, trackPrefsBoxName});
});
test('an unusable name is ignored and reported', () {
for (final bad in ['../x', 'a b', 'x' * 33, 'é']) {
final env = {profileEnv: bad};
expect(AppDirs.fromEnv(support: support, env: env).profile, isNull, reason: bad);
expect(AppDirs.invalidProfile(env), bad);
}
expect(AppDirs.invalidProfile(const {profileEnv: ''}), isNull, reason: 'empty is just unset');
expect(AppDirs.invalidProfile(const {profileEnv: 'ok_1-2'}), isNull);
test('old support folders next to the new one', () {
expect(legacySupportDirs('/home/u/.local/share/dev.foxyz.starsync', windows: false), [
'/home/u/.local/share/moe.senkochan.watch_together',
'/home/u/.local/share/watch_together',
]);
expect(legacySupportDirs(r'C:\Users\u\AppData\Roaming\foxyz\Starsync', windows: true),
[r'C:\Users\u\AppData\Roaming\com.example\watch_together']);
});
group('migration from before the rename', () {
late Directory home;
late String share, oldSupport, support, docs, hive;
setUp(() async {
home = await Directory.systemTemp.createTemp('starsync-mig');
share = '${home.path}/.local/share';
oldSupport = '$share/moe.senkochan.watch_together';
support = '$share/dev.foxyz.starsync';
docs = '${home.path}/Documents';
hive = '$support/hive';
// The old support folder as 1.2.x left it.
await Directory('$oldSupport/yt-dlp').create(recursive: true);
await File('$oldSupport/yt-dlp/cookies.txt').writeAsString('# Netscape cookies');
await File('$oldSupport/yt-dlp/yt-dlp').writeAsString('bin');
await Directory('$oldSupport/logs').create();
await File('$oldSupport/logs/app-2026-01-01.log').writeAsString('log');
await Directory('$oldSupport/shaders').create();
await File('$oldSupport/shaders/a.glsl').writeAsString('shader');
await Directory('$oldSupport/profiles/b/hive').create(recursive: true);
await File('$oldSupport/profiles/b/hive/settings.hive').writeAsString('b-settings');
if (!Platform.isWindows) {
await Process.run('chmod', ['600', '$oldSupport/yt-dlp/cookies.txt']);
await Process.run('chmod', ['755', '$oldSupport/yt-dlp/yt-dlp']);
await Link('$oldSupport/yt-dlp/current').create('yt-dlp');
}
// The documents folder: this app's boxes and another app's.
await Directory(docs).create(recursive: true);
for (final n in ['settings', 'jellyfin', 'files', 'track_prefs', 'connect_form', 'app', 'credentials']) {
await File('$docs/$n.hive').writeAsString('$n-data');
await File('$docs/$n.lock').writeAsString('');
}
// path_provider creates the new folder before the app looks.
await Directory(support).create(recursive: true);
});
tearDown(() => home.delete(recursive: true));
Future<List<String>> migrate() => migrateLegacyData(
support: support, hive: hive, oldSupport: legacySupportDirs(support, windows: false), documents: docs);
test('copies the support folder and only this app\'s boxes, once', () async {
final lines = await migrate();
expect(lines, hasLength(2));
expect(lines.first, contains(oldSupport));
expect(lines.last, contains('5 Hive boxes'));
expect(lines.join(), isNot(contains('Netscape')), reason: 'paths and counts, never contents');
expect(await File('$support/yt-dlp/cookies.txt').readAsString(), '# Netscape cookies');
expect(await File('$support/logs/app-2026-01-01.log').readAsString(), 'log');
expect(await File('$support/shaders/a.glsl').readAsString(), 'shader');
expect(await File('$support/profiles/b/hive/settings.hive').readAsString(), 'b-settings',
reason: 'profiles come along with the support folder');
if (!Platform.isWindows) {
expect(File('$support/yt-dlp/cookies.txt').statSync().mode & 0x1ff, 0x180, reason: '0600 kept');
expect(File('$support/yt-dlp/yt-dlp').statSync().mode & 0x40, isNonZero, reason: 'exec bit kept');
expect(await Link('$support/yt-dlp/current').target(), 'yt-dlp');
}
for (final n in appHiveBoxes) {
expect(await File('$hive/$n.hive').readAsString(), '$n-data');
expect(File('$hive/$n.lock').existsSync(), isTrue);
}
expect(File('$hive/app.hive').existsSync(), isFalse);
expect(File('$hive/credentials.hive').existsSync(), isFalse);
// Copied, not moved: 1.2.x keeps working.
expect(File('$docs/settings.hive').existsSync(), isTrue);
expect(File('$oldSupport/yt-dlp/cookies.txt').existsSync(), isTrue);
final marker = jsonDecode(File('$support/$migrationMarker').readAsStringSync()) as Map;
expect(marker['migratedFrom'], oldSupport);
expect(marker['hiveFrom'], docs);
expect(marker['files'], greaterThan(0));
// Once: a later change on the old side is not copied again.
await File('$docs/settings.hive').writeAsString('changed by 1.2.x');
await File('$hive/settings.hive').delete();
expect(await migrate(), isEmpty);
expect(File('$hive/settings.hive').existsSync(), isFalse);
});
test('never overwrites what the new folder already has', () async {
await Directory(hive).create(recursive: true);
await File('$hive/settings.hive').writeAsString('new');
await Directory('$support/yt-dlp').create();
await File('$support/yt-dlp/cookies.txt').writeAsString('new cookies');
await migrate();
expect(await File('$hive/settings.hive').readAsString(), 'new');
expect(await File('$support/yt-dlp/cookies.txt').readAsString(), 'new cookies');
expect(await File('$hive/jellyfin.hive').readAsString(), 'jellyfin-data');
});
test('a fresh install finds nothing and still marks it done', () async {
await Directory(oldSupport).delete(recursive: true);
await Directory(docs).delete(recursive: true);
expect(await migrate(), ['migration: no data from before the rename']);
final marker = jsonDecode(File('$support/$migrationMarker').readAsStringSync()) as Map;
expect(marker['migratedFrom'], isNull);
expect(marker['hiveFrom'], isNull);
});
});
}
+5 -5
View File
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/shell/app_shell.dart';
import 'package:watch_together/src/state/session_state.dart';
import 'package:watch_together/src/update/update_controller.dart';
import 'package:watch_together/src/widgets/app_keys.dart';
import 'package:watch_together/src/widgets/text_focus.dart';
import 'package:starsync/src/shell/app_shell.dart';
import 'package:starsync/src/state/session_state.dart';
import 'package:starsync/src/update/update_controller.dart';
import 'package:starsync/src/widgets/app_keys.dart';
import 'package:starsync/src/widgets/text_focus.dart';
void main() {
final session = SessionState.instance;
+2 -2
View File
@@ -5,8 +5,8 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/sync/avatar_cache.dart';
import 'package:watch_together/src/sync/sync_client.dart';
import 'package:starsync/src/sync/avatar_cache.dart';
import 'package:starsync/src/sync/sync_client.dart';
Future<void> _poll(FutureOr<bool> Function() cond, String what) async {
final deadline = DateTime.now().add(const Duration(seconds: 10));
+3 -3
View File
@@ -5,9 +5,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/playback/media_marks.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:watch_together/src/sync/state_diff.dart';
import 'package:starsync/src/playback/media_marks.dart';
import 'package:starsync/src/session/session_coordinator.dart';
import 'package:starsync/src/sync/state_diff.dart';
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
+2 -2
View File
@@ -3,8 +3,8 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/sync/state_diff.dart';
import 'package:watch_together/src/sync/sync_client.dart';
import 'package:starsync/src/sync/state_diff.dart';
import 'package:starsync/src/sync/sync_client.dart';
Future<void> _poll(FutureOr<bool> Function() cond, String what) async {
final end = DateTime.now().add(const Duration(seconds: 10));
+8 -8
View File
@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:signals/signals_flutter.dart';
import 'package:watch_together/src/controls/controls_probe.dart';
import 'package:watch_together/src/controls/model/controls_layout.dart';
import 'package:watch_together/src/controls/model/controls_model.dart';
import 'package:watch_together/src/controls/model/controls_prefs.dart';
import 'package:watch_together/src/controls/session_controls.dart';
import 'package:watch_together/src/playback/media_marks.dart';
import 'package:watch_together/src/state/session_state.dart';
import 'package:watch_together/src/state/settings.dart';
import 'package:starsync/src/controls/controls_probe.dart';
import 'package:starsync/src/controls/model/controls_layout.dart';
import 'package:starsync/src/controls/model/controls_model.dart';
import 'package:starsync/src/controls/model/controls_prefs.dart';
import 'package:starsync/src/controls/session_controls.dart';
import 'package:starsync/src/playback/media_marks.dart';
import 'package:starsync/src/state/session_state.dart';
import 'package:starsync/src/state/settings.dart';
// The controls model and its layout spec: which controls the bar has, in
// which order, what goes to ⚙; and both skins drawing the same order.
+4 -4
View File
@@ -8,10 +8,10 @@ import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:hive_ce/hive.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:watch_together/src/sfx/sfx.dart';
import 'package:watch_together/src/state/session_state.dart';
import 'package:watch_together/src/sync/sync_client.dart';
import 'package:starsync/src/session/session_coordinator.dart';
import 'package:starsync/src/sfx/sfx.dart';
import 'package:starsync/src/state/session_state.dart';
import 'package:starsync/src/sync/sync_client.dart';
Future<void> _poll(
FutureOr<bool> Function() cond,
+2 -2
View File
@@ -3,8 +3,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/session/delivery.dart';
import 'package:watch_together/src/shell/delivery_line.dart';
import 'package:starsync/src/session/delivery.dart';
import 'package:starsync/src/shell/delivery_line.dart';
MemberView _m(String id, String name, {double buffered = 30, bool host = false, int rate = 0}) =>
MemberView(id: id, name: name, ready: true, inSet: true, buffered: buffered, stale: false, host: host, rate: rate);
+37
View File
@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:starsync/src/platform/desktop_entry.dart';
void main() {
test('writes the Starsync entry, removes only the old id\'s files', () async {
final tmp = await Directory.systemTemp.createTemp('starsync-entry');
try {
final root = '${tmp.path}/install';
await Directory('$root/lib').create(recursive: true);
await Directory('$root/data/flutter_assets/assets/icon').create(recursive: true);
await File('$root/data/flutter_assets/assets/icon/icon_48.png').writeAsString('png');
final share = '${tmp.path}/share';
final apps = Directory('$share/applications')..createSync(recursive: true);
final oldEntry = File('${apps.path}/$legacyAppId.desktop')..writeAsStringSync('old');
final other = File('${apps.path}/other.app.desktop')..writeAsStringSync('keep');
final oldIcon = File('$share/icons/hicolor/48x48/apps/$legacyAppId.png')
..createSync(recursive: true);
final otherIcon = File('$share/icons/hicolor/48x48/apps/other.png')..createSync();
await installDesktopEntry(executable: '$root/starsync', dataHome: share, force: true);
expect(oldEntry.existsSync(), isFalse);
expect(oldIcon.existsSync(), isFalse);
expect(other.existsSync(), isTrue);
expect(otherIcon.existsSync(), isTrue);
final entry = File('${apps.path}/dev.foxyz.starsync.desktop').readAsStringSync();
expect(entry, contains('Exec="$root/starsync"'));
expect(entry, contains('StartupWMClass=dev.foxyz.starsync'));
expect(entry, contains('Icon=dev.foxyz.starsync'));
expect(File('$share/icons/hicolor/48x48/apps/dev.foxyz.starsync.png').existsSync(), isTrue);
} finally {
await tmp.delete(recursive: true);
}
}, skip: Platform.isLinux ? null : 'Linux launcher entry');
}
+1 -1
View File
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:starsync/src/session/session_coordinator.dart';
const _mediaPath = '/home/foxyz/Videos/chiyo.mp4'; // 13.8 s
+5 -5
View File
@@ -7,11 +7,11 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_ce/hive.dart';
import 'package:watch_together/src/files/folder_store.dart';
import 'package:watch_together/src/files/scanner.dart';
import 'package:watch_together/src/files/thumbnails.dart';
import 'package:watch_together/src/files/ui/file_tiles.dart';
import 'package:watch_together/src/files/ui/files_page.dart';
import 'package:starsync/src/files/folder_store.dart';
import 'package:starsync/src/files/scanner.dart';
import 'package:starsync/src/files/thumbnails.dart';
import 'package:starsync/src/files/ui/file_tiles.dart';
import 'package:starsync/src/files/ui/files_page.dart';
// The Files page over a real (tiny) folder tree: the scan runs in-line
// instead of in an isolate and thumbnails come from a fake loader, so
+2 -2
View File
@@ -3,8 +3,8 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/files/name_parser.dart';
import 'package:watch_together/src/files/scanner.dart';
import 'package:starsync/src/files/name_parser.dart';
import 'package:starsync/src/files/scanner.dart';
late Directory _root;
+1 -1
View File
@@ -2,7 +2,7 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_ce/hive.dart';
import 'package:watch_together/src/files/folder_store.dart';
import 'package:starsync/src/files/folder_store.dart';
void main() {
test('folders and recent files persist across a reload', () async {
+1 -1
View File
@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:media_kit/media_kit.dart';
import 'package:watch_together/src/files/thumbnails.dart';
import 'package:starsync/src/files/thumbnails.dart';
// Real libmpv: frames grabbed with vo=null + screenshot-to-file. The test
// media comes from ffmpeg (this machine only; the app never needs it).
+1 -1
View File
@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/shell/handoff.dart';
import 'package:starsync/src/shell/handoff.dart';
// What a drop or a paste is: decided from paths and text alone (the file
// system stands behind a fake).
+10 -10
View File
@@ -5,17 +5,17 @@ import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/files/ui/files_page.dart';
import 'package:watch_together/src/schedule/ui/schedule_page.dart';
import 'package:starsync/src/files/ui/files_page.dart';
import 'package:starsync/src/schedule/ui/schedule_page.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/screens/home/health_probe.dart';
import 'package:watch_together/src/screens/home/media_picker.dart';
import 'package:watch_together/src/screens/home/update_indicator.dart';
import 'package:watch_together/src/shell/app_shell.dart';
import 'package:watch_together/src/shell/library_link.dart' show soloPlayHint;
import 'package:watch_together/src/state/session_state.dart';
import 'package:watch_together/src/state/settings.dart';
import 'package:watch_together/src/update/update_controller.dart';
import 'package:starsync/src/screens/home/health_probe.dart';
import 'package:starsync/src/screens/home/media_picker.dart';
import 'package:starsync/src/screens/home/update_indicator.dart';
import 'package:starsync/src/shell/app_shell.dart';
import 'package:starsync/src/shell/library_link.dart' show soloPlayHint;
import 'package:starsync/src/state/session_state.dart';
import 'package:starsync/src/state/settings.dart';
import 'package:starsync/src/update/update_controller.dart';
RecentServer join(String host, String port, [String secret = '']) =>
RecentServer(hosted: false, host: host, port: port, secret: secret);
@@ -4,10 +4,10 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_ce/hive.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/jellyfin/format.dart';
import 'package:watch_together/src/jellyfin/sign_in.dart';
import 'package:watch_together/src/screens/home/media_picker.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/jellyfin/format.dart';
import 'package:starsync/src/jellyfin/sign_in.dart';
import 'package:starsync/src/screens/home/media_picker.dart';
/// Captured Jellyfin 10.11 answers shared with packages/core's client tests.
String jellyfinFixture(String name) =>
+6 -6
View File
@@ -5,12 +5,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/jellyfin/accounts_panel.dart';
import 'package:watch_together/src/jellyfin/browser.dart';
import 'package:watch_together/src/jellyfin/browse_scope.dart';
import 'package:watch_together/src/jellyfin/picker_source.dart';
import 'package:watch_together/src/state/settings.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/jellyfin/accounts_panel.dart';
import 'package:starsync/src/jellyfin/browser.dart';
import 'package:starsync/src/jellyfin/browse_scope.dart';
import 'package:starsync/src/jellyfin/picker_source.dart';
import 'package:starsync/src/state/settings.dart';
import 'jellyfin_accounts_test.dart'
show FakeJellyfin, arifuretaId, fixtureServerId, linkedAccounts, season2Id;
+6 -6
View File
@@ -8,12 +8,12 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/jellyfin/session_media.dart';
import 'package:watch_together/src/jellyfin/track_prefs.dart';
import 'package:watch_together/src/playback/media_marks.dart';
import 'package:watch_together/src/screens/home/picked_media.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/jellyfin/session_media.dart';
import 'package:starsync/src/jellyfin/track_prefs.dart';
import 'package:starsync/src/playback/media_marks.dart';
import 'package:starsync/src/screens/home/picked_media.dart';
import 'package:starsync/src/session/session_coordinator.dart';
// Two account holders and a guest watch Jellyfin episodes together over a
// real sync server, real mpv and a local fake Jellyfin (never a real one):
+11 -11
View File
@@ -6,17 +6,17 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/jellyfin/browse_scope.dart';
import 'package:watch_together/src/jellyfin/browser.dart';
import 'package:watch_together/src/jellyfin/progress.dart';
import 'package:watch_together/src/jellyfin/session_media.dart';
import 'package:watch_together/src/jellyfin/track_prefs.dart';
import 'package:watch_together/src/screens/home/media_picker.dart';
import 'package:watch_together/src/screens/session/jellyfin_widgets.dart';
import 'package:watch_together/src/session/delivery.dart';
import 'package:watch_together/src/session/session_coordinator.dart' show AutoAdvance;
import 'package:watch_together/src/shell/library_tab.dart' show putUpJellyfinMedia;
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/jellyfin/browse_scope.dart';
import 'package:starsync/src/jellyfin/browser.dart';
import 'package:starsync/src/jellyfin/progress.dart';
import 'package:starsync/src/jellyfin/session_media.dart';
import 'package:starsync/src/jellyfin/track_prefs.dart';
import 'package:starsync/src/screens/home/media_picker.dart';
import 'package:starsync/src/screens/session/jellyfin_widgets.dart';
import 'package:starsync/src/session/delivery.dart';
import 'package:starsync/src/session/session_coordinator.dart' show AutoAdvance;
import 'package:starsync/src/shell/library_tab.dart' show putUpJellyfinMedia;
import 'jellyfin_accounts_test.dart';
import 'jellyfin_browser_test.dart' show app, setSize, settle;
+4 -4
View File
@@ -2,10 +2,10 @@ import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/home_rows.dart';
import 'package:watch_together/src/screens/home/media_picker.dart';
import 'package:watch_together/src/shell/library_link.dart';
import 'package:watch_together/src/shell/library_tab.dart';
import 'package:starsync/src/jellyfin/home_rows.dart';
import 'package:starsync/src/screens/home/media_picker.dart';
import 'package:starsync/src/shell/library_link.dart';
import 'package:starsync/src/shell/library_tab.dart';
final now = DateTime.utc(2026, 10, 8, 12);
DateTime daysAgo(num d) => now.subtract(Duration(minutes: (d * 24 * 60).round()));
+6 -6
View File
@@ -5,12 +5,12 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/jellyfin/browse_scope.dart';
import 'package:watch_together/src/jellyfin/details_panel.dart';
import 'package:watch_together/src/screens/home/media_picker.dart';
import 'package:watch_together/src/shell/library_link.dart';
import 'package:watch_together/src/shell/library_tab.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/jellyfin/browse_scope.dart';
import 'package:starsync/src/jellyfin/details_panel.dart';
import 'package:starsync/src/screens/home/media_picker.dart';
import 'package:starsync/src/shell/library_link.dart';
import 'package:starsync/src/shell/library_tab.dart';
import 'jellyfin_accounts_test.dart'
show FakeJellyfin, arifuretaId, fixtureServerId, jellyfinFixture, linkedAccounts, season2Id;
+3 -3
View File
@@ -1,8 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/session_media.dart';
import 'package:watch_together/src/jellyfin/track_prefs.dart';
import 'package:watch_together/src/playback/media_marks.dart';
import 'package:starsync/src/jellyfin/session_media.dart';
import 'package:starsync/src/jellyfin/track_prefs.dart';
import 'package:starsync/src/playback/media_marks.dart';
Duration s(num secs) => Duration(milliseconds: (secs * 1000).round());
Duration m(int min, [int sec = 0]) => Duration(minutes: min, seconds: sec);
+10 -10
View File
@@ -1,16 +1,16 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/controls/frame.dart';
import 'package:watch_together/src/controls/minimal/style.dart';
import 'package:watch_together/src/controls/minimal/timeline.dart';
import 'package:watch_together/src/controls/model/controls_prefs.dart';
import 'package:watch_together/src/controls/volume.dart';
import 'package:watch_together/src/playback/media_marks.dart';
import 'package:watch_together/src/shell/settings_tab.dart';
import 'package:watch_together/src/state/settings.dart';
import 'package:watch_together/src/update/update_controller.dart';
import 'package:watch_together/src/widgets/app_menu.dart';
import 'package:starsync/src/controls/frame.dart';
import 'package:starsync/src/controls/minimal/style.dart';
import 'package:starsync/src/controls/minimal/timeline.dart';
import 'package:starsync/src/controls/model/controls_prefs.dart';
import 'package:starsync/src/controls/volume.dart';
import 'package:starsync/src/playback/media_marks.dart';
import 'package:starsync/src/shell/settings_tab.dart';
import 'package:starsync/src/state/settings.dart';
import 'package:starsync/src/update/update_controller.dart';
import 'package:starsync/src/widgets/app_menu.dart';
// The Minimal skin's pieces and the shared controls frame (never the
// session screen or a media_kit `Video`).
+2 -2
View File
@@ -9,8 +9,8 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart' show FeedMessage;
import 'package:watch_together/src/session/notifier.dart';
import 'package:watch_together/src/sfx/sfx.dart';
import 'package:starsync/src/session/notifier.dart';
import 'package:starsync/src/sfx/sfx.dart';
final _at = DateTime.utc(2026, 1, 1);
+2 -2
View File
@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/session/now_playing.dart';
import 'package:watch_together/src/session/now_playing_info.dart';
import 'package:starsync/src/session/now_playing.dart';
import 'package:starsync/src/session/now_playing_info.dart';
void main() {
group('jellyfinTitle', () {
+5 -5
View File
@@ -2,11 +2,11 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart' show JellyfinStreamType;
import 'package:watch_together/src/controls/control_menus.dart';
import 'package:watch_together/src/controls/standard/seek_bar.dart';
import 'package:watch_together/src/jellyfin/track_prefs.dart';
import 'package:watch_together/src/playback/media_marks.dart';
import 'package:watch_together/src/screens/session/player_controls.dart';
import 'package:starsync/src/controls/control_menus.dart';
import 'package:starsync/src/controls/standard/seek_bar.dart';
import 'package:starsync/src/jellyfin/track_prefs.dart';
import 'package:starsync/src/playback/media_marks.dart';
import 'package:starsync/src/screens/session/player_controls.dart';
// The control-bar pieces alone (never the session screen or a `Video`):
// the Standard seek bar, the skip pill and the shared track / chapter
+12 -12
View File
@@ -6,18 +6,18 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/controls/frame.dart';
import 'package:watch_together/src/controls/model/controls_prefs.dart';
import 'package:watch_together/src/controls/player_settings.dart';
import 'package:watch_together/src/jellyfin/track_prefs.dart';
import 'package:watch_together/src/schedule/animeflow.dart';
import 'package:watch_together/src/shaders/shader_presets.dart';
import 'package:watch_together/src/shell/avatar.dart';
import 'package:watch_together/src/shell/party_strip.dart';
import 'package:watch_together/src/shell/settings_tab.dart';
import 'package:watch_together/src/state/session_state.dart';
import 'package:watch_together/src/state/settings.dart';
import 'package:watch_together/src/update/update_controller.dart';
import 'package:starsync/src/controls/frame.dart';
import 'package:starsync/src/controls/model/controls_prefs.dart';
import 'package:starsync/src/controls/player_settings.dart';
import 'package:starsync/src/jellyfin/track_prefs.dart';
import 'package:starsync/src/schedule/animeflow.dart';
import 'package:starsync/src/shaders/shader_presets.dart';
import 'package:starsync/src/shell/avatar.dart';
import 'package:starsync/src/shell/party_strip.dart';
import 'package:starsync/src/shell/settings_tab.dart';
import 'package:starsync/src/state/session_state.dart';
import 'package:starsync/src/state/settings.dart';
import 'package:starsync/src/update/update_controller.dart';
// The player settings card (both skins, never a media_kit `Video`), the
// Settings pages, the host flag in the party UI and invite addresses.
+10 -10
View File
@@ -5,16 +5,16 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/controls/frame.dart';
import 'package:watch_together/src/controls/minimal/style.dart';
import 'package:watch_together/src/controls/minimal/timeline.dart';
import 'package:watch_together/src/session/health.dart';
import 'package:watch_together/src/session/now_playing.dart';
import 'package:watch_together/src/shell/now_playing_title.dart';
import 'package:watch_together/src/shell/player_status.dart';
import 'package:watch_together/src/shell/session_player.dart';
import 'package:watch_together/src/shell/start_banner.dart';
import 'package:watch_together/src/state/session_state.dart';
import 'package:starsync/src/controls/frame.dart';
import 'package:starsync/src/controls/minimal/style.dart';
import 'package:starsync/src/controls/minimal/timeline.dart';
import 'package:starsync/src/session/health.dart';
import 'package:starsync/src/session/now_playing.dart';
import 'package:starsync/src/shell/now_playing_title.dart';
import 'package:starsync/src/shell/player_status.dart';
import 'package:starsync/src/shell/session_player.dart';
import 'package:starsync/src/shell/start_banner.dart';
import 'package:starsync/src/state/session_state.dart';
// The player's title block, start card and status pill (never a media_kit
// `Video`).
+1 -1
View File
@@ -2,7 +2,7 @@
// the janitor become server state.
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/queue/queue_logic.dart';
import 'package:starsync/src/queue/queue_logic.dart';
MemberView member(String id,
{bool stale = false, bool out = false, Map<String, bool> jf = const {}, String name = ''}) =>
+4 -4
View File
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/queue/queue_ui.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:watch_together/src/shell/library_link.dart' show LinkPreview;
import 'package:watch_together/src/widgets/text_focus.dart';
import 'package:starsync/src/queue/queue_ui.dart';
import 'package:starsync/src/session/session_coordinator.dart';
import 'package:starsync/src/shell/library_link.dart' show LinkPreview;
import 'package:starsync/src/widgets/text_focus.dart';
void main() {
testWidgets('Up next popover: holds the keys, Ctrl+V adds with a preview row, + Add link opens a field',
+8 -8
View File
@@ -8,14 +8,14 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/jellyfin/session_media.dart';
import 'package:watch_together/src/jellyfin/track_prefs.dart';
import 'package:watch_together/src/playback/yt_dlp_app.dart';
import 'package:watch_together/src/queue/queue_controller.dart' show WarmState;
import 'package:watch_together/src/screens/home/picked_media.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:watch_together/src/session/switch_trace.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/jellyfin/session_media.dart';
import 'package:starsync/src/jellyfin/track_prefs.dart';
import 'package:starsync/src/playback/yt_dlp_app.dart';
import 'package:starsync/src/queue/queue_controller.dart' show WarmState;
import 'package:starsync/src/screens/home/picked_media.dart';
import 'package:starsync/src/session/session_coordinator.dart';
import 'package:starsync/src/session/switch_trace.dart';
// How long an Up next switch takes, end to end, over real mpv and a real
// sync server: two Jellyfin account holders and a guest (through the
+1 -1
View File
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/playback/playback_controller.dart';
import 'package:starsync/src/playback/playback_controller.dart';
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
+5 -5
View File
@@ -9,11 +9,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/jellyfin/track_prefs.dart';
import 'package:watch_together/src/playback/yt_dlp_app.dart';
import 'package:watch_together/src/screens/home/picked_media.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/jellyfin/track_prefs.dart';
import 'package:starsync/src/playback/yt_dlp_app.dart';
import 'package:starsync/src/screens/home/picked_media.dart';
import 'package:starsync/src/session/session_coordinator.dart';
// The relay with real coordinators, real mpv and a dedicated server that
// has no file and no Jellyfin login of its own (the VPS setup): a guest's
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/files/name_parser.dart';
import 'package:starsync/src/files/name_parser.dart';
void _expect(
String name,
+4 -4
View File
@@ -3,8 +3,8 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/schedule/anilist.dart';
import 'package:watch_together/src/session/media_art.dart';
import 'package:starsync/src/schedule/anilist.dart';
import 'package:starsync/src/session/media_art.dart';
import 'schedule_fake_server.dart';
@@ -45,7 +45,7 @@ void main() {
cacheDir: () async => cache,
delay: (d) async => delays.add(d),
now: () => now,
userAgent: 'watch-together/test',
userAgent: 'Starsync/test',
);
setUp(() async {
@@ -73,7 +73,7 @@ void main() {
expect(server.seen, hasLength(2));
final first = jsonDecode(server.seen.first.body) as Map;
expect(first['variables'], {'from': from.millisecondsSinceEpoch ~/ 1000 - 1, 'to': to.millisecondsSinceEpoch ~/ 1000, 'page': 1});
expect(server.seen.first.headers.value(HttpHeaders.userAgentHeader), 'watch-together/test');
expect(server.seen.first.headers.value(HttpHeaders.userAgentHeader), 'Starsync/test');
expect(s.airings.map((a) => a.anilistId), [1, 4]);
final a = s.airings.first;
expect(a.title, 'Show');
@@ -2,8 +2,8 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/schedule/animeflow.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/schedule/animeflow.dart';
import 'schedule_fake_server.dart';
@@ -83,7 +83,7 @@ void main() {
final c = client();
final me = await c.me();
expect(server.seen.single.headers.value('x-emby-token'), token);
expect(server.seen.single.headers.value(HttpHeaders.userAgentHeader), startsWith('watch-together/'));
expect(server.seen.single.headers.value(HttpHeaders.userAgentHeader), startsWith('Starsync/'));
expect(me.userId, 'u1');
expect(me.name, 'Foxyz');
expect(me.scopes, {AnimeFlowScope.view, AnimeFlowScope.request});
+3 -3
View File
@@ -2,9 +2,9 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/schedule/anilist.dart';
import 'package:watch_together/src/schedule/animeflow.dart';
import 'package:watch_together/src/schedule/schedule_model.dart';
import 'package:starsync/src/schedule/anilist.dart';
import 'package:starsync/src/schedule/animeflow.dart';
import 'package:starsync/src/schedule/schedule_model.dart';
import 'schedule_fake_server.dart';
+7 -7
View File
@@ -6,13 +6,13 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/schedule/anilist.dart';
import 'package:watch_together/src/schedule/animeflow.dart';
import 'package:watch_together/src/schedule/schedule_model.dart';
import 'package:watch_together/src/schedule/ui/schedule_actions.dart';
import 'package:watch_together/src/schedule/ui/schedule_page.dart';
import 'package:watch_together/src/screens/home/picked_media.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/schedule/anilist.dart';
import 'package:starsync/src/schedule/animeflow.dart';
import 'package:starsync/src/schedule/schedule_model.dart';
import 'package:starsync/src/schedule/ui/schedule_actions.dart';
import 'package:starsync/src/schedule/ui/schedule_page.dart';
import 'package:starsync/src/screens/home/picked_media.dart';
import 'schedule_fake_server.dart';
+2 -2
View File
@@ -5,8 +5,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:watch_together/src/sync/state_diff.dart';
import 'package:starsync/src/session/session_coordinator.dart';
import 'package:starsync/src/sync/state_diff.dart';
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
+5 -5
View File
@@ -9,11 +9,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:hive_ce/hive.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/screens/home/picked_media.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:watch_together/src/state/server_isolate.dart';
import 'package:watch_together/src/state/session_state.dart';
import 'package:starsync/src/jellyfin/accounts.dart';
import 'package:starsync/src/screens/home/picked_media.dart';
import 'package:starsync/src/session/session_coordinator.dart';
import 'package:starsync/src/state/server_isolate.dart';
import 'package:starsync/src/state/session_state.dart';
Future<void> _poll(
FutureOr<bool> Function() cond,
+2 -2
View File
@@ -2,8 +2,8 @@
// IdleTracker and unreadyForNewMedia give way to auto sit-out;
// linkDisplayName stays.
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/session/idle.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:starsync/src/session/idle.dart';
import 'package:starsync/src/session/session_coordinator.dart';
void main() {
group('IdleTracker', () {
@@ -0,0 +1,57 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_ce/hive.dart';
import 'package:starsync/src/state/settings_schema.dart';
void main() {
group('settings schema', () {
late Directory dir;
setUp(() async {
dir = await Directory.systemTemp.createTemp('starsync-schema');
Hive.init(dir.path);
});
tearDown(() async {
await Hive.close();
await dir.delete(recursive: true);
});
test('a box without a version is at 1 and gets stamped', () async {
final box = await Hive.openBox('settings');
expect(await migrateSettings(box), isEmpty);
expect(box.get(settingsSchemaKey), settingsSchemaVersion);
});
test('steps run in order, only past the stored version', () async {
final box = await Hive.openBox('settings');
await box.put(settingsSchemaKey, 1);
await box.put('old', 'x');
final order = <int>[];
final steps = [
SettingsMigration(3, 'c', (b) async {
order.add(3);
await b.put('c', b.get('new'));
}),
SettingsMigration(2, 'rename old to new', (b) async {
order.add(2);
await b.put('new', b.get('old'));
await b.delete('old');
}),
SettingsMigration(4, 'not yet', (b) async => order.add(4)),
];
final ran = await migrateSettings(box, steps: steps, current: 3);
expect(order, [2, 3]);
expect(ran, ['settings schema 2: rename old to new', 'settings schema 3: c']);
expect(box.get('c'), 'x');
expect(box.get(settingsSchemaKey), 3);
expect(await migrateSettings(box, steps: steps, current: 3), isEmpty);
});
test('a box from a newer build is left as it is', () async {
final box = await Hive.openBox('settings');
await box.put(settingsSchemaKey, 9);
expect(await migrateSettings(box), hasLength(1));
expect(box.get(settingsSchemaKey), 9);
});
});
}
+3 -3
View File
@@ -1,9 +1,9 @@
import 'dart:math' as math;
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/sfx/sfx.dart';
import 'package:watch_together/src/sfx/sound_bank.dart';
import 'package:watch_together/src/sfx/synth.dart';
import 'package:starsync/src/sfx/sfx.dart';
import 'package:starsync/src/sfx/sound_bank.dart';
import 'package:starsync/src/sfx/synth.dart';
double _pitch(List<double> x, int from) {
final c = <double>[];
+4 -4
View File
@@ -2,10 +2,10 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:watch_together/src/playback/playback_controller.dart';
import 'package:watch_together/src/shaders/shader_assets.dart';
import 'package:watch_together/src/shaders/shader_presets.dart';
import 'package:watch_together/src/state/settings.dart';
import 'package:starsync/src/playback/playback_controller.dart';
import 'package:starsync/src/shaders/shader_assets.dart';
import 'package:starsync/src/shaders/shader_presets.dart';
import 'package:starsync/src/state/settings.dart';
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
+3 -3
View File
@@ -2,9 +2,9 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_ce/hive.dart';
import 'package:watch_together/src/shaders/shader_assets.dart';
import 'package:watch_together/src/shaders/shader_presets.dart';
import 'package:watch_together/src/state/settings.dart';
import 'package:starsync/src/shaders/shader_assets.dart';
import 'package:starsync/src/shaders/shader_presets.dart';
import 'package:starsync/src/state/settings.dart';
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
+12 -12
View File
@@ -4,18 +4,18 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/screens/home/profile.dart';
import 'package:watch_together/src/shell/app_shell.dart';
import 'package:watch_together/src/shell/avatar.dart';
import 'package:watch_together/src/shell/motion.dart';
import 'package:watch_together/src/shell/party_strip.dart';
import 'package:watch_together/src/shell/profile_card.dart';
import 'package:watch_together/src/shell/session_player.dart';
import 'package:watch_together/src/shell/settings_tab.dart' show SettingsTab;
import 'package:watch_together/src/shell/toasts.dart';
import 'package:watch_together/src/session/session_coordinator.dart' show ToastItem;
import 'package:watch_together/src/state/session_state.dart';
import 'package:watch_together/src/update/update_controller.dart';
import 'package:starsync/src/screens/home/profile.dart';
import 'package:starsync/src/shell/app_shell.dart';
import 'package:starsync/src/shell/avatar.dart';
import 'package:starsync/src/shell/motion.dart';
import 'package:starsync/src/shell/party_strip.dart';
import 'package:starsync/src/shell/profile_card.dart';
import 'package:starsync/src/shell/session_player.dart';
import 'package:starsync/src/shell/settings_tab.dart' show SettingsTab;
import 'package:starsync/src/shell/toasts.dart';
import 'package:starsync/src/session/session_coordinator.dart' show ToastItem;
import 'package:starsync/src/state/session_state.dart';
import 'package:starsync/src/update/update_controller.dart';
/// Stands in for media_kit's `Video` (which hangs flutter_test) and
/// counts how often one is created.
+4 -4
View File
@@ -3,10 +3,10 @@ import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_soloud/flutter_soloud.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/platform/audio/sound_engine.dart';
import 'package:watch_together/src/sfx/sfx.dart';
import 'package:watch_together/src/sfx/sound_bank.dart';
import 'package:watch_together/src/sfx/synth_sfx.dart';
import 'package:starsync/src/platform/audio/sound_engine.dart';
import 'package:starsync/src/sfx/sfx.dart';
import 'package:starsync/src/sfx/sound_bank.dart';
import 'package:starsync/src/sfx/synth_sfx.dart';
// Needs a real audio output: opens the device through flutter_soloud and
// plays every sound of every preset once, quietly (WT_SFX_VOLUME, default
+5 -5
View File
@@ -1,11 +1,11 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/platform/audio/sound_engine.dart';
import 'package:watch_together/src/sfx/sfx.dart';
import 'package:watch_together/src/sfx/sound_bank.dart';
import 'package:watch_together/src/sfx/synth.dart';
import 'package:watch_together/src/sfx/synth_sfx.dart';
import 'package:starsync/src/platform/audio/sound_engine.dart';
import 'package:starsync/src/sfx/sfx.dart';
import 'package:starsync/src/sfx/sound_bank.dart';
import 'package:starsync/src/sfx/synth.dart';
import 'package:starsync/src/sfx/synth_sfx.dart';
class _FakeBackend implements AudioBackend {
final bool failInit;
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/sync/state_diff.dart';
import 'package:starsync/src/sync/state_diff.dart';
StateBroadcast _state({
int eventId = 0,
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:starsync/src/session/session_coordinator.dart';
const _port = 18080;
const _secret = 'e2e-secret';
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:watch_together/src/widgets/text_focus.dart';
import 'package:starsync/src/widgets/text_focus.dart';
void main() {
// Regression: the old FocusNode-parent walk never met EditableText, so
+55 -41
View File
@@ -4,7 +4,7 @@ import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/update/update_controller.dart';
import 'package:starsync/src/update/update_controller.dart';
// Work dir base: WT_TEST_TMP or the system temp. Staging lives inside
// the install root, so the result doesn't depend on which filesystem
@@ -24,49 +24,39 @@ final _posixOnly = Platform.isWindows ? 'POSIX install layout' : null;
bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0;
// Builds the release archive exactly like tool/package_linux.sh:
// bundle copied into <stage>/watch-together-<ver>/, then
// `tar -czf out -C <stage> watch-together-<ver>`. [zip]: like
// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only).
// bundle copied into <stage>/starsync-<ver>/ with the launcher by the
// old name (lib/src/update/bundle/watch_together, the shipped file), then
// `tar -czf out -C <stage> starsync-<ver>`. [app]: the app binary's
// content.
Future<List<int>> _releaseTarball(Directory work,
{String version = '0.2.0',
bool windows = false,
bool bundle = true,
bool zip = false}) async {
final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}');
final folder = Directory('${stage.path}/watch-together-$version');
{String version = '0.2.0', String app = 'binary-v2'}) async {
final stage = Directory('${work.path}/pkg-$version-lin');
final folder = Directory('${stage.path}/starsync-$version');
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
await File('${folder.path}/data/flutter_assets/fresh.cfg')
.writeAsString('fresh-config');
if (windows) {
await File('${folder.path}/watch_together.exe').writeAsString('binary-v2');
await File('${folder.path}/flutter_windows.dll').writeAsString('dll-v2');
} else {
await File('${folder.path}/watch_together').writeAsString('binary-v2');
if (bundle) {
await Directory('${folder.path}/lib').create();
await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2');
await Link('${folder.path}/lib/libapp.so').create('libapp.so.1');
}
await File('${folder.path}/update.sh')
.writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n');
if (!Platform.isWindows) {
final r = await Process.run('chmod',
['755', '${folder.path}/watch_together', '${folder.path}/update.sh']);
expect(r.exitCode, 0);
}
await File('${folder.path}/starsync').writeAsString(app);
await Directory('${folder.path}/lib').create();
await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2');
await Link('${folder.path}/lib/libapp.so').create('libapp.so.1');
for (final f in ['update.sh', 'watch_together', 'watch-together-server']) {
await File('lib/src/update/bundle/$f').copy('${folder.path}/$f');
}
final out = File('${work.path}/out-$version.${zip ? 'zip' : 'tar.gz'}');
final r = await Process.run('tar', [
...zip ? ['-a', '-c', '-f'] : ['-czf'],
out.path,
'-C',
stage.path,
'watch-together-$version',
]);
if (!Platform.isWindows) {
final r = await Process.run('chmod', [
'755',
for (final f in ['starsync', 'update.sh', 'watch_together', 'watch-together-server'])
'${folder.path}/$f'
]);
expect(r.exitCode, 0);
}
final out = File('${work.path}/out-$version.tar.gz');
final r = await Process.run('tar', ['-czf', out.path, '-C', stage.path, 'starsync-$version']);
expect(r.exitCode, 0, reason: '${r.stderr}');
return out.readAsBytes();
}
// An install as 1.2.x left it: the binary is still called watch_together.
Future<Directory> _fakeRoot(Directory work, {bool windows = false}) async {
final root = Directory('${work.path}/install');
await Directory('${root.path}/data').create(recursive: true);
@@ -105,12 +95,12 @@ class _Server {
req.response.write(jsonEncode({
'version': '0.2.0',
'protocol': RelayConstants.protoVersion,
'url': '$base/watch-together-0.2.0-linux-x64.tar.gz',
'url': '$base/starsync-0.2.0-linux-x64.tar.gz',
'sha256': sha256.convert(tarball).toString(),
'notes': 'test build',
...manifestExtra,
}));
} else if (req.uri.path == '/watch-together-0.2.0-linux-x64.tar.gz') {
} else if (req.uri.path == '/starsync-0.2.0-linux-x64.tar.gz') {
tarHits++;
req.response.contentLength = tarball.length;
req.response.add(tarball);
@@ -145,12 +135,14 @@ void main() {
test('check, download with progress, apply, restart same binary',
() async {
final root = await _fakeRoot(work);
// Already renamed: started as starsync.
await File('${root.path}/watch_together').rename('${root.path}/starsync');
final restarts = <(String, List<String>)>[];
launchArgs = const ['--some-flag'];
final c = UpdateController(
currentVersion: '0.1.0',
manifestUrl: server.manifestUrl,
executablePath: '${root.path}/watch_together',
executablePath: '${root.path}/starsync',
restart: (exe, args) async => restarts.add((exe, args)));
await c.checkOnce();
expect(c.status.value.phase, UpdatePhase.available);
@@ -164,13 +156,35 @@ void main() {
await c.install();
expect(progress.last, 1.0);
expect(c.status.value.phase, UpdatePhase.restarting);
expect(await File('${root.path}/watch_together').readAsString(),
expect(await File('${root.path}/starsync').readAsString(),
'binary-v2');
expect(_isExec('${root.path}/watch_together'), isTrue);
expect(restarts.single.$1, '${root.path}/watch_together');
expect(_isExec('${root.path}/starsync'), isTrue);
expect(restarts.single.$1, '${root.path}/starsync');
expect(restarts.single.$2, ['--some-flag']);
}, skip: _posixOnly);
test('started by the old name: restarts the new binary; the old name launches it',
() async {
server.tarball = await _releaseTarball(work,
version: '0.2.1', app: '#!/bin/sh\necho "starsync \$*"\n');
final root = await _fakeRoot(work);
final restarts = <String>[];
final c = UpdateController(
currentVersion: '0.1.0',
manifestUrl: server.manifestUrl,
executablePath: '${root.path}/watch_together',
restart: (exe, _) async => restarts.add(exe));
await c.checkOnce();
await c.install();
expect(c.status.value.phase, UpdatePhase.restarting);
expect(restarts.single, '${root.path}/starsync');
// What a 1.2.x app restarts after this update, and the VPS server
// name: both launch the new binaries.
final app = await Process.run('${root.path}/watch_together', ['--a']);
expect('${app.stdout}'.trim(), 'starsync --a', reason: '${app.stderr}');
expect(_isExec('${root.path}/watch-together-server'), isTrue);
}, skip: _posixOnly);
test('newer protocol is a required update, cannot be dismissed',
() async {
server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1};
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_relay_core/core.dart';
import 'package:watch_together/src/sync/user_seek.dart';
import 'package:starsync/src/sync/user_seek.dart';
StateBroadcast _state({int eventId = 1, String? seekBy, int? seekSeq}) =>
StateBroadcast(
+2 -2
View File
@@ -6,8 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:watch_together/src/session/session_coordinator.dart';
import 'package:watch_together/src/sync/state_diff.dart';
import 'package:starsync/src/session/session_coordinator.dart';
import 'package:starsync/src/sync/state_diff.dart';
const _mediaPath = '/home/foxyz/Videos/chiyo.mp4';
+5 -5
View File
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart' show VideoState;
import 'package:watch_together/src/controls/model/controls_prefs.dart';
import 'package:watch_together/src/controls/frame.dart';
import 'package:watch_together/src/shell/session_player.dart';
import 'package:watch_together/src/state/session_state.dart';
import 'package:watch_together/src/state/settings.dart';
import 'package:starsync/src/controls/model/controls_prefs.dart';
import 'package:starsync/src/controls/frame.dart';
import 'package:starsync/src/shell/session_player.dart';
import 'package:starsync/src/state/session_state.dart';
import 'package:starsync/src/state/settings.dart';
// The wheel anywhere on the full player flashes the volume readout, in
// both skins, windowed and fullscreen, with the controls up or hidden.
+1 -1
View File
@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:media_kit/media_kit.dart';
import 'package:watch_together/src/playback/playback_controller.dart';
import 'package:starsync/src/playback/playback_controller.dart';
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
const _referer = 'https://www.bilibili.com/';
+2 -2
View File
@@ -1,10 +1,10 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(watch_together LANGUAGES CXX)
project(starsync LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "watch_together")
set(BINARY_NAME "starsync")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
+6 -6
View File
@@ -89,13 +89,13 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "watch_together" "\0"
VALUE "CompanyName", "foxyz" "\0"
VALUE "FileDescription", "Starsync" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "watch_together" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "watch_together.exe" "\0"
VALUE "ProductName", "watch_together" "\0"
VALUE "InternalName", "Starsync" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 foxyz. All rights reserved." "\0"
VALUE "OriginalFilename", "starsync.exe" "\0"
VALUE "ProductName", "Starsync" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
+1 -1
View File
@@ -16,7 +16,7 @@ namespace {
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
constexpr const wchar_t kWindowClassName[] = L"STARSYNC_WINDOW";
/// Registry key for app theme preference.
///