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:
1 parent
f4ca02c3cf
commit
51c1c5ab78
96 files changed
+1169
-489
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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 "$@"
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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).
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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 = ''}) =>
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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>[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
Reference in new issue
Block a user