Restructure step R: the app is Starsync (dev.foxyz.starsync)

- 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

+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/';