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
+1145
-465
No files matched your search
@@ -35,9 +35,9 @@ jobs:
|
|||||||
- run: dart test
|
- run: dart test
|
||||||
working-directory: packages/server
|
working-directory: packages/server
|
||||||
# Compiling the dedicated server proves it stays Flutter-free.
|
# Compiling the dedicated server proves it stays Flutter-free.
|
||||||
- run: mkdir -p build && dart compile exe bin/server.dart -o build/watch-together-server
|
- run: mkdir -p build && dart compile exe bin/server.dart -o build/starsync-server
|
||||||
working-directory: packages/server
|
working-directory: packages/server
|
||||||
- run: build/watch-together-server --help
|
- run: build/starsync-server --help
|
||||||
working-directory: packages/server
|
working-directory: packages/server
|
||||||
|
|
||||||
# The updater's Windows apply path (running exe parked as .old) only
|
# The updater's Windows apply path (running exe parked as .old) only
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
VER="${GITHUB_REF_NAME#v}"
|
VER="${GITHUB_REF_NAME#v}"
|
||||||
dart tool/release_publish.dart "$GITHUB_REF_NAME" "dist/watch-together-$VER-linux-x64.tar.gz"
|
dart tool/release_publish.dart "$GITHUB_REF_NAME" "dist/starsync-$VER-linux-x64.tar.gz"
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
# yuuka-windows: host-mode runner (no containers), runs as SYSTEM, has
|
# yuuka-windows: host-mode runner (no containers), runs as SYSTEM, has
|
||||||
@@ -73,5 +73,5 @@ jobs:
|
|||||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
$Ver = $env:GITHUB_REF_NAME.TrimStart('v')
|
$Ver = $env:GITHUB_REF_NAME.TrimStart('v')
|
||||||
dart tool/release_publish.dart $env:GITHUB_REF_NAME "dist/watch-together-$Ver-windows-x64.zip"
|
dart tool/release_publish.dart $env:GITHUB_REF_NAME "dist/starsync-$Ver-windows-x64.zip"
|
||||||
if ($LASTEXITCODE) { exit 1 }
|
if ($LASTEXITCODE) { exit 1 }
|
||||||
@@ -11,9 +11,9 @@ everyone's play, pause and seek stay together.
|
|||||||
Why not Jellyfin SyncPlay: it runs in the browser and its sync is poor.
|
Why not Jellyfin SyncPlay: it runs in the browser and its sync is poor.
|
||||||
This is a desktop app that drives mpv.
|
This is a desktop app that drives mpv.
|
||||||
|
|
||||||
Name: `watch-together` is a placeholder and the final name is undecided
|
Up to 1.2.x the app was called `watch_together` (its files
|
||||||
(`sync-relay` is the working name). It appears in file names and the
|
`watch-together-*`); the repository and the update manifest keep their old
|
||||||
update manifest until that is settled.
|
addresses for now.
|
||||||
|
|
||||||
- Desktop only (Linux and Windows), always mpv. No web clients.
|
- Desktop only (Linux and Windows), always mpv. No web clients.
|
||||||
- No transcoding. Everyone plays the original file.
|
- No transcoding. Everyone plays the original file.
|
||||||
@@ -25,21 +25,33 @@ How it fits together: [docs/HOW-IT-WORKS.md](docs/HOW-IT-WORKS.md).
|
|||||||
|
|
||||||
From the Releases page of this repo:
|
From the Releases page of this repo:
|
||||||
|
|
||||||
- Linux: `watch-together-<version>-linux-x64.tar.gz`. Extract, run
|
- Linux: `starsync-<version>-linux-x64.tar.gz`. Extract, run
|
||||||
`watch_together`. On start it adds itself to your app launcher (and its
|
`starsync`. On start it adds itself to your app launcher (and its
|
||||||
icon), so the window shows its own icon on Wayland. Needs mpv's library installed (`mpv` or `libmpv2`).
|
icon), so the window shows its own icon on Wayland. Needs mpv's library installed (`mpv` or `libmpv2`).
|
||||||
- Windows: `watch-together-<version>-windows-x64.zip`. Extract, run
|
- Windows: `starsync-<version>-windows-x64.zip`. Extract, run
|
||||||
`watch_together.exe`. mpv is bundled. New and not yet tried on a real
|
`starsync.exe`. mpv is bundled. New and not yet tried on a real
|
||||||
Windows desktop; reports welcome.
|
Windows desktop; reports welcome.
|
||||||
|
|
||||||
|
The folder also holds `watch_together(.exe)` and
|
||||||
|
`watch-together-server(.exe)`, the names up to 1.2.x: they start the new
|
||||||
|
programs, so an update from 1.2.x restarts fine and old shortcuts and
|
||||||
|
service files keep working.
|
||||||
|
|
||||||
|
Your data (settings, sign-ins, folders, logs, yt-dlp and its cookies) lives
|
||||||
|
in `~/.local/share/dev.foxyz.starsync` on Linux and
|
||||||
|
`%APPDATA%\foxyz\Starsync` on Windows. The first start after 1.2.x copies
|
||||||
|
it over from the old places (`~/.local/share/moe.senkochan.watch_together`
|
||||||
|
or `%APPDATA%\com.example\watch_together`, and the app's files in your
|
||||||
|
Documents folder) and leaves the originals where they were.
|
||||||
|
|
||||||
Update from the app: the app looks for a new version at start and every
|
Update from the app: the app looks for a new version at start and every
|
||||||
few hours, and when one exists a band across the top of the window says so
|
few hours, and when one exists a band across the top of the window says so
|
||||||
(never over a playing video). "Update and restart" downloads,
|
(never over a playing video). "Update and restart" downloads,
|
||||||
verifies, applies in place and restarts; "Later" hides it until the next
|
verifies, applies in place and restarts; "Later" hides it until the next
|
||||||
check; the install folder must be writable by you. The version you run is
|
check; the install folder must be writable by you. The version you run is
|
||||||
under the Settings tab. Settings has Check now and an auto-check toggle. From a terminal:
|
under the Settings tab. Settings has Check now and an auto-check toggle. From a terminal:
|
||||||
`./watch-together-server --upgrade` (no display needed, so it works on a
|
`./starsync-server --upgrade` (no display needed, so it works on a
|
||||||
server box too), `watch_together --upgrade`, or the shipped `update.sh` /
|
server box too), `./starsync --upgrade`, or the shipped `update.sh` /
|
||||||
`update.bat` (which run the first). Each updates the whole folder, app and
|
`update.bat` (which run the first). Each updates the whole folder, app and
|
||||||
server; restart whichever is running to use the new version. A
|
server; restart whichever is running to use the new version. A
|
||||||
release with a newer protocol is a required update: everyone on a server
|
release with a newer protocol is a required update: everyone on a server
|
||||||
@@ -221,11 +233,11 @@ cover; one you started shows how far you got.
|
|||||||
## Dedicated server
|
## Dedicated server
|
||||||
|
|
||||||
For an always-on box (no display needed): the release tarball also contains
|
For an always-on box (no display needed): the release tarball also contains
|
||||||
`watch-together-server`, a small standalone server without the app. Run it,
|
`starsync-server`, a small standalone server without the app. Run it,
|
||||||
then everyone (you included) uses Join.
|
then everyone (you included) uses Join.
|
||||||
|
|
||||||
```
|
```
|
||||||
./watch-together-server --port 8080 --secret <password>
|
./starsync-server --port 8080 --secret <password>
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Meaning |
|
| Flag | Default | Meaning |
|
||||||
@@ -245,9 +257,11 @@ then everyone (you included) uses Join.
|
|||||||
|
|
||||||
It prints `READY port=... name=...` once listening and stops cleanly on
|
It prints `READY port=... name=...` once listening and stops cleanly on
|
||||||
Ctrl+C or SIGTERM. A dedicated server has no host.
|
Ctrl+C or SIGTERM. A dedicated server has no host.
|
||||||
To run it as a service, unpack the tarball to `/opt/watch-together` and use
|
To run it as a service, unpack the tarball to `/opt/starsync` and use
|
||||||
[deploy/watch-together.service](deploy/watch-together.service) (copy it to
|
[deploy/starsync.service](deploy/starsync.service) (copy it to
|
||||||
`/etc/systemd/system/`, edit the flags, `systemctl enable --now watch-together`).
|
`/etc/systemd/system/`, edit the flags, `systemctl enable --now starsync`).
|
||||||
|
A service set up before the rename (`/opt/watch-together`, running
|
||||||
|
`watch-together-server`) keeps working as it is.
|
||||||
|
|
||||||
Something not playing? Settings → About → **Copy diagnostics** puts the
|
Something not playing? Settings → About → **Copy diagnostics** puts the
|
||||||
app's recent log (passwords, tokens and cookies left out) on the
|
app's recent log (passwords, tokens and cookies left out) on the
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import 'package:media_kit/media_kit.dart';
|
|||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
import 'src/platform/app_dirs.dart';
|
|
||||||
import 'src/platform/app_log.dart';
|
import 'src/platform/app_log.dart';
|
||||||
import 'src/platform/app_shutdown.dart';
|
import 'src/platform/app_shutdown.dart';
|
||||||
|
import 'src/platform/desktop/desktop_app_dirs.dart';
|
||||||
import 'src/platform/desktop_entry.dart';
|
import 'src/platform/desktop_entry.dart';
|
||||||
import 'src/screens/home/ui.dart';
|
import 'src/screens/home/ui.dart';
|
||||||
import 'src/session/app_events.dart' show UpdateAvailable;
|
import 'src/session/app_events.dart' show UpdateAvailable;
|
||||||
@@ -42,10 +42,9 @@ Future<void> main(List<String> args) async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
final dirs = await AppDirs.resolve();
|
final dirs = await const DesktopAppDirs().resolve();
|
||||||
await startAppLogging(dirs);
|
await startAppLogging(dirs);
|
||||||
// An absolute subDir replaces the documents folder (package:path join).
|
Hive.init(dirs.hive);
|
||||||
await Hive.initFlutter(dirs.hive);
|
|
||||||
await AppSettings.instance.ensureLoaded();
|
await AppSettings.instance.ensureLoaded();
|
||||||
// Close button / Alt+F4: the orderly close ([shutDownApp]), so others
|
// Close button / Alt+F4: the orderly close ([shutDownApp]), so others
|
||||||
// see "left", a hosted server shuts down and no player outlives the
|
// 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 'package:sync_relay_core/core.dart';
|
||||||
|
|
||||||
import '../screens/home/ui.dart' show appName, displayVersion;
|
import '../screens/home/ui.dart' show appName, displayVersion;
|
||||||
import 'app_dirs.dart';
|
import 'ports/app_dirs.dart';
|
||||||
import '../state/session_state.dart';
|
import '../state/session_state.dart';
|
||||||
|
|
||||||
// The app's log on disk (<app support>/logs/app-YYYY-MM-DD.log; core's
|
// 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} '
|
_appLog.info('$appName ${displayVersion()} on ${Platform.operatingSystem} '
|
||||||
'${Platform.operatingSystemVersion}, pid $pid, Dart ${Platform.version.split(' ').first}'
|
'${Platform.operatingSystemVersion}, pid $pid, Dart ${Platform.version.split(' ').first}'
|
||||||
'${dirs.profile == null ? '' : ', profile ${dirs.profile}'}');
|
'${dirs.profile == null ? '' : ', profile ${dirs.profile}'}');
|
||||||
|
for (final line in dirs.migrated) {
|
||||||
|
_appLog.info(line);
|
||||||
|
}
|
||||||
final bad = AppDirs.invalidProfile(Platform.environment);
|
final bad = AppDirs.invalidProfile(Platform.environment);
|
||||||
if (bad != null) _appLog.warning('$profileEnv="$bad" ignored: use letters, digits, - and _ (at most 32)');
|
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
|
/// The app's id: GTK's application id, so the Wayland `app_id` the
|
||||||
/// compositor matches to [desktopFileName] for the window's icon.
|
/// compositor matches to [desktopFileName] for the window's icon.
|
||||||
const appId = 'moe.senkochan.watch_together';
|
const appId = 'dev.foxyz.starsync';
|
||||||
const desktopFileName = '$appId.desktop';
|
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
|
/// Linux release installs (an unpacked tarball) carry no launcher entry, so
|
||||||
/// Wayland shows a generic icon. On start: write the `.desktop` file into
|
/// Wayland shows a generic icon. On start: write the `.desktop` file into
|
||||||
/// `~/.local/share/applications` (Exec = this binary) and the icon into
|
/// `~/.local/share/applications` (Exec = this binary) and the icon into
|
||||||
/// the hicolor theme, only when something changed (a moved install, a new
|
/// the hicolor theme, only when something changed (a moved install, a new
|
||||||
/// icon). Never in debug runs; failures only log.
|
/// icon). The entry and icons of [legacyAppId] go. Never in debug runs
|
||||||
Future<void> installDesktopEntry({String? executable, String? dataHome, String? home}) async {
|
/// (unless [force], for tests); failures only log.
|
||||||
if (!Platform.isLinux || kDebugMode) return;
|
Future<void> installDesktopEntry(
|
||||||
|
{String? executable, String? dataHome, String? home, @visibleForTesting bool force = false}) async {
|
||||||
|
if (!Platform.isLinux || (kDebugMode && !force)) return;
|
||||||
try {
|
try {
|
||||||
final exe = executable ?? Platform.resolvedExecutable;
|
final exe = executable ?? Platform.resolvedExecutable;
|
||||||
final root = findInstallRoot(exe, windows: false);
|
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'] ??
|
final share = dataHome ?? Platform.environment['XDG_DATA_HOME'] ??
|
||||||
'${home ?? Platform.environment['HOME']}/.local/share';
|
'${home ?? Platform.environment['HOME']}/.local/share';
|
||||||
final icons = '${root.path}/data/flutter_assets/assets/icon';
|
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');
|
final src = File('$icons/icon_$size.png');
|
||||||
if (!src.existsSync()) continue;
|
if (!src.existsSync()) continue;
|
||||||
final dst = File('$share/icons/hicolor/${size}x$size/apps/$appId.png');
|
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
|
@visibleForTesting
|
||||||
String desktopEntryText(String exe) => '''[Desktop Entry]
|
String desktopEntryText(String exe) => '''[Desktop Entry]
|
||||||
Type=Application
|
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 'system_notifications.dart';
|
||||||
import 'url_opener.dart';
|
import 'url_opener.dart';
|
||||||
|
|
||||||
|
export 'app_dirs.dart';
|
||||||
export 'folder_access.dart';
|
export 'folder_access.dart';
|
||||||
export 'link_resolver.dart';
|
export 'link_resolver.dart';
|
||||||
export 'shell_port.dart';
|
export 'shell_port.dart';
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class AniListClient {
|
|||||||
_cacheDir = cacheDir ?? _defaultCacheDir,
|
_cacheDir = cacheDir ?? _defaultCacheDir,
|
||||||
_delay = delay ?? Future<void>.delayed,
|
_delay = delay ?? Future<void>.delayed,
|
||||||
_now = now ?? DateTime.now,
|
_now = now ?? DateTime.now,
|
||||||
userAgent = userAgent ?? 'watch-together/$appVersion';
|
userAgent = userAgent ?? 'Starsync/$appVersion';
|
||||||
|
|
||||||
final Uri url;
|
final Uri url;
|
||||||
final Duration memoryTtl;
|
final Duration memoryTtl;
|
||||||
|
|||||||
@@ -474,7 +474,7 @@ class AnimeFlowClient {
|
|||||||
_ownsHttp = http == null,
|
_ownsHttp = http == null,
|
||||||
_delay = delay ?? Future<void>.delayed,
|
_delay = delay ?? Future<void>.delayed,
|
||||||
_now = now ?? DateTime.now,
|
_now = now ?? DateTime.now,
|
||||||
userAgent = userAgent ?? 'watch-together/$appVersion';
|
userAgent = userAgent ?? 'Starsync/$appVersion';
|
||||||
|
|
||||||
/// Authenticated with the linked Jellyfin account on the same domain
|
/// Authenticated with the linked Jellyfin account on the same domain
|
||||||
/// (see [tokenFor]), looked up on every call so a re-link applies.
|
/// (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 '../controls/model/controls_prefs.dart';
|
||||||
import '../shaders/shader_presets.dart';
|
import '../shaders/shader_presets.dart';
|
||||||
|
import 'settings_schema.dart';
|
||||||
|
|
||||||
// App preferences (Settings page), one Hive box. Signals start at their
|
// App preferences (Settings page), one Hive box. Signals start at their
|
||||||
// defaults and take the stored values once [AppSettings.ensureLoaded]
|
// defaults and take the stored values once [AppSettings.ensureLoaded]
|
||||||
@@ -134,6 +135,9 @@ class AppSettings {
|
|||||||
Future<void> _loadNow() async {
|
Future<void> _loadNow() async {
|
||||||
try {
|
try {
|
||||||
final box = _box = await Hive.openBox(settingsBoxName);
|
final box = _box = await Hive.openBox(settingsBoxName);
|
||||||
|
for (final line in await migrateSettings(box)) {
|
||||||
|
debugPrint(line);
|
||||||
|
}
|
||||||
final mode = box.get(_keyHwdecMode);
|
final mode = box.get(_keyHwdecMode);
|
||||||
hwdecMode.value = HwdecMode.values
|
hwdecMode.value = HwdecMode.values
|
||||||
.where((m) => m.name == mode)
|
.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
|
@echo off
|
||||||
rem Update this install in place (same as clicking Update in the app).
|
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
|
pause
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Update this install in place (same as clicking Update in the app).
|
# Update this install in place (same as clicking Update in the app).
|
||||||
# The server binary runs the updater: it needs no GTK/display (VPS).
|
# 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');
|
_log.info('updated to ${m.version}, restarting');
|
||||||
status.value =
|
status.value =
|
||||||
UpdateStatus(UpdatePhase.restarting, manifest: m, canInstall: true);
|
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 name of the executable created for the application. Change this to change
|
||||||
# the on-disk name of your application.
|
# 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:
|
# The unique GTK application identifier for this application. See:
|
||||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
# 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
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
# versions of CMake.
|
# versions of CMake.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: watch_together
|
name: starsync
|
||||||
description: Watch-together desktop app (working title, repo sync_relay_project). One binary.
|
description: Starsync desktop app (repo sync_relay_project). One binary.
|
||||||
publish_to: none
|
publish_to: none
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
// restructure: rewrite in step R (Rename): Hive moves into the support
|
import 'dart:convert';
|
||||||
// folder for the normal app too.
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
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() {
|
void main() {
|
||||||
final sep = Platform.pathSeparator;
|
final sep = Platform.pathSeparator;
|
||||||
const support = '/s';
|
const support = '/s';
|
||||||
|
|
||||||
test('no profile: today\'s layout', () {
|
group('AppDirs', () {
|
||||||
|
test('no profile: Hive and logs in the support folder', () {
|
||||||
final d = AppDirs.fromEnv(support: support, env: const {});
|
final d = AppDirs.fromEnv(support: support, env: const {});
|
||||||
expect(d.profile, isNull);
|
expect(d.profile, isNull);
|
||||||
expect(d.hive, isNull, reason: "Hive's default folder");
|
expect(d.hive, '$support${sep}hive');
|
||||||
expect(d.logs, '$support${sep}logs');
|
expect(d.logs, '$support${sep}logs');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,4 +39,120 @@ void main() {
|
|||||||
expect(AppDirs.invalidProfile(const {profileEnv: ''}), isNull, reason: 'empty is just unset');
|
expect(AppDirs.invalidProfile(const {profileEnv: ''}), isNull, reason: 'empty is just unset');
|
||||||
expect(AppDirs.invalidProfile(const {profileEnv: 'ok_1-2'}), isNull);
|
expect(AppDirs.invalidProfile(const {profileEnv: 'ok_1-2'}), isNull);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the migrated boxes are the ones the app opens', () {
|
||||||
|
expect(appHiveBoxes.toSet(),
|
||||||
|
{settingsBoxName, connectFormBoxName, filesBoxName, jellyfinBoxName, trackPrefsBoxName});
|
||||||
|
});
|
||||||
|
|
||||||
|
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/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/shell/app_shell.dart';
|
import 'package:starsync/src/shell/app_shell.dart';
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
import 'package:watch_together/src/update/update_controller.dart';
|
import 'package:starsync/src/update/update_controller.dart';
|
||||||
import 'package:watch_together/src/widgets/app_keys.dart';
|
import 'package:starsync/src/widgets/app_keys.dart';
|
||||||
import 'package:watch_together/src/widgets/text_focus.dart';
|
import 'package:starsync/src/widgets/text_focus.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final session = SessionState.instance;
|
final session = SessionState.instance;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
import 'package:watch_together/src/sync/avatar_cache.dart';
|
import 'package:starsync/src/sync/avatar_cache.dart';
|
||||||
import 'package:watch_together/src/sync/sync_client.dart';
|
import 'package:starsync/src/sync/sync_client.dart';
|
||||||
|
|
||||||
Future<void> _poll(FutureOr<bool> Function() cond, String what) async {
|
Future<void> _poll(FutureOr<bool> Function() cond, String what) async {
|
||||||
final deadline = DateTime.now().add(const Duration(seconds: 10));
|
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:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
import 'package:watch_together/src/playback/media_marks.dart';
|
import 'package:starsync/src/playback/media_marks.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart';
|
import 'package:starsync/src/session/session_coordinator.dart';
|
||||||
import 'package:watch_together/src/sync/state_diff.dart';
|
import 'package:starsync/src/sync/state_diff.dart';
|
||||||
|
|
||||||
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
|
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:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
import 'package:watch_together/src/sync/state_diff.dart';
|
import 'package:starsync/src/sync/state_diff.dart';
|
||||||
import 'package:watch_together/src/sync/sync_client.dart';
|
import 'package:starsync/src/sync/sync_client.dart';
|
||||||
|
|
||||||
Future<void> _poll(FutureOr<bool> Function() cond, String what) async {
|
Future<void> _poll(FutureOr<bool> Function() cond, String what) async {
|
||||||
final end = DateTime.now().add(const Duration(seconds: 10));
|
final end = DateTime.now().add(const Duration(seconds: 10));
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:signals/signals_flutter.dart';
|
import 'package:signals/signals_flutter.dart';
|
||||||
import 'package:watch_together/src/controls/controls_probe.dart';
|
import 'package:starsync/src/controls/controls_probe.dart';
|
||||||
import 'package:watch_together/src/controls/model/controls_layout.dart';
|
import 'package:starsync/src/controls/model/controls_layout.dart';
|
||||||
import 'package:watch_together/src/controls/model/controls_model.dart';
|
import 'package:starsync/src/controls/model/controls_model.dart';
|
||||||
import 'package:watch_together/src/controls/model/controls_prefs.dart';
|
import 'package:starsync/src/controls/model/controls_prefs.dart';
|
||||||
import 'package:watch_together/src/controls/session_controls.dart';
|
import 'package:starsync/src/controls/session_controls.dart';
|
||||||
import 'package:watch_together/src/playback/media_marks.dart';
|
import 'package:starsync/src/playback/media_marks.dart';
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
import 'package:watch_together/src/state/settings.dart';
|
import 'package:starsync/src/state/settings.dart';
|
||||||
|
|
||||||
// The controls model and its layout spec: which controls the bar has, in
|
// 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.
|
// 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_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
import 'package:hive_ce/hive.dart';
|
import 'package:hive_ce/hive.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart';
|
import 'package:starsync/src/session/session_coordinator.dart';
|
||||||
import 'package:watch_together/src/sfx/sfx.dart';
|
import 'package:starsync/src/sfx/sfx.dart';
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
import 'package:watch_together/src/sync/sync_client.dart';
|
import 'package:starsync/src/sync/sync_client.dart';
|
||||||
|
|
||||||
Future<void> _poll(
|
Future<void> _poll(
|
||||||
FutureOr<bool> Function() cond,
|
FutureOr<bool> Function() cond,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/session/delivery.dart';
|
import 'package:starsync/src/session/delivery.dart';
|
||||||
import 'package:watch_together/src/shell/delivery_line.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 _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);
|
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:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.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
|
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/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:hive_ce/hive.dart';
|
import 'package:hive_ce/hive.dart';
|
||||||
import 'package:watch_together/src/files/folder_store.dart';
|
import 'package:starsync/src/files/folder_store.dart';
|
||||||
import 'package:watch_together/src/files/scanner.dart';
|
import 'package:starsync/src/files/scanner.dart';
|
||||||
import 'package:watch_together/src/files/thumbnails.dart';
|
import 'package:starsync/src/files/thumbnails.dart';
|
||||||
import 'package:watch_together/src/files/ui/file_tiles.dart';
|
import 'package:starsync/src/files/ui/file_tiles.dart';
|
||||||
import 'package:watch_together/src/files/ui/files_page.dart';
|
import 'package:starsync/src/files/ui/files_page.dart';
|
||||||
|
|
||||||
// The Files page over a real (tiny) folder tree: the scan runs in-line
|
// 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
|
// instead of in an isolate and thumbnails come from a fake loader, so
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/files/name_parser.dart';
|
import 'package:starsync/src/files/name_parser.dart';
|
||||||
import 'package:watch_together/src/files/scanner.dart';
|
import 'package:starsync/src/files/scanner.dart';
|
||||||
|
|
||||||
late Directory _root;
|
late Directory _root;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:hive_ce/hive.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() {
|
void main() {
|
||||||
test('folders and recent files persist across a reload', () async {
|
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:flutter_test/flutter_test.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:media_kit/media_kit.dart';
|
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
|
// Real libmpv: frames grabbed with vo=null + screenshot-to-file. The test
|
||||||
// media comes from ffmpeg (this machine only; the app never needs it).
|
// media comes from ffmpeg (this machine only; the app never needs it).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
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
|
// What a drop or a paste is: decided from paths and text alone (the file
|
||||||
// system stands behind a fake).
|
// system stands behind a fake).
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ import 'dart:io' show Platform;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/files/ui/files_page.dart';
|
import 'package:starsync/src/files/ui/files_page.dart';
|
||||||
import 'package:watch_together/src/schedule/ui/schedule_page.dart';
|
import 'package:starsync/src/schedule/ui/schedule_page.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/screens/home/health_probe.dart';
|
import 'package:starsync/src/screens/home/health_probe.dart';
|
||||||
import 'package:watch_together/src/screens/home/media_picker.dart';
|
import 'package:starsync/src/screens/home/media_picker.dart';
|
||||||
import 'package:watch_together/src/screens/home/update_indicator.dart';
|
import 'package:starsync/src/screens/home/update_indicator.dart';
|
||||||
import 'package:watch_together/src/shell/app_shell.dart';
|
import 'package:starsync/src/shell/app_shell.dart';
|
||||||
import 'package:watch_together/src/shell/library_link.dart' show soloPlayHint;
|
import 'package:starsync/src/shell/library_link.dart' show soloPlayHint;
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
import 'package:watch_together/src/state/settings.dart';
|
import 'package:starsync/src/state/settings.dart';
|
||||||
import 'package:watch_together/src/update/update_controller.dart';
|
import 'package:starsync/src/update/update_controller.dart';
|
||||||
|
|
||||||
RecentServer join(String host, String port, [String secret = '']) =>
|
RecentServer join(String host, String port, [String secret = '']) =>
|
||||||
RecentServer(hosted: false, host: host, port: port, secret: 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:flutter_test/flutter_test.dart';
|
||||||
import 'package:hive_ce/hive.dart';
|
import 'package:hive_ce/hive.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/jellyfin/format.dart';
|
import 'package:starsync/src/jellyfin/format.dart';
|
||||||
import 'package:watch_together/src/jellyfin/sign_in.dart';
|
import 'package:starsync/src/jellyfin/sign_in.dart';
|
||||||
import 'package:watch_together/src/screens/home/media_picker.dart';
|
import 'package:starsync/src/screens/home/media_picker.dart';
|
||||||
|
|
||||||
/// Captured Jellyfin 10.11 answers shared with packages/core's client tests.
|
/// Captured Jellyfin 10.11 answers shared with packages/core's client tests.
|
||||||
String jellyfinFixture(String name) =>
|
String jellyfinFixture(String name) =>
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts_panel.dart';
|
import 'package:starsync/src/jellyfin/accounts_panel.dart';
|
||||||
import 'package:watch_together/src/jellyfin/browser.dart';
|
import 'package:starsync/src/jellyfin/browser.dart';
|
||||||
import 'package:watch_together/src/jellyfin/browse_scope.dart';
|
import 'package:starsync/src/jellyfin/browse_scope.dart';
|
||||||
import 'package:watch_together/src/jellyfin/picker_source.dart';
|
import 'package:starsync/src/jellyfin/picker_source.dart';
|
||||||
import 'package:watch_together/src/state/settings.dart';
|
import 'package:starsync/src/state/settings.dart';
|
||||||
|
|
||||||
import 'jellyfin_accounts_test.dart'
|
import 'jellyfin_accounts_test.dart'
|
||||||
show FakeJellyfin, arifuretaId, fixtureServerId, linkedAccounts, season2Id;
|
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:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/jellyfin/session_media.dart';
|
import 'package:starsync/src/jellyfin/session_media.dart';
|
||||||
import 'package:watch_together/src/jellyfin/track_prefs.dart';
|
import 'package:starsync/src/jellyfin/track_prefs.dart';
|
||||||
import 'package:watch_together/src/playback/media_marks.dart';
|
import 'package:starsync/src/playback/media_marks.dart';
|
||||||
import 'package:watch_together/src/screens/home/picked_media.dart';
|
import 'package:starsync/src/screens/home/picked_media.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart';
|
import 'package:starsync/src/session/session_coordinator.dart';
|
||||||
|
|
||||||
// Two account holders and a guest watch Jellyfin episodes together over a
|
// 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):
|
// 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/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/jellyfin/browse_scope.dart';
|
import 'package:starsync/src/jellyfin/browse_scope.dart';
|
||||||
import 'package:watch_together/src/jellyfin/browser.dart';
|
import 'package:starsync/src/jellyfin/browser.dart';
|
||||||
import 'package:watch_together/src/jellyfin/progress.dart';
|
import 'package:starsync/src/jellyfin/progress.dart';
|
||||||
import 'package:watch_together/src/jellyfin/session_media.dart';
|
import 'package:starsync/src/jellyfin/session_media.dart';
|
||||||
import 'package:watch_together/src/jellyfin/track_prefs.dart';
|
import 'package:starsync/src/jellyfin/track_prefs.dart';
|
||||||
import 'package:watch_together/src/screens/home/media_picker.dart';
|
import 'package:starsync/src/screens/home/media_picker.dart';
|
||||||
import 'package:watch_together/src/screens/session/jellyfin_widgets.dart';
|
import 'package:starsync/src/screens/session/jellyfin_widgets.dart';
|
||||||
import 'package:watch_together/src/session/delivery.dart';
|
import 'package:starsync/src/session/delivery.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart' show AutoAdvance;
|
import 'package:starsync/src/session/session_coordinator.dart' show AutoAdvance;
|
||||||
import 'package:watch_together/src/shell/library_tab.dart' show putUpJellyfinMedia;
|
import 'package:starsync/src/shell/library_tab.dart' show putUpJellyfinMedia;
|
||||||
|
|
||||||
import 'jellyfin_accounts_test.dart';
|
import 'jellyfin_accounts_test.dart';
|
||||||
import 'jellyfin_browser_test.dart' show app, setSize, settle;
|
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:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/jellyfin/home_rows.dart';
|
import 'package:starsync/src/jellyfin/home_rows.dart';
|
||||||
import 'package:watch_together/src/screens/home/media_picker.dart';
|
import 'package:starsync/src/screens/home/media_picker.dart';
|
||||||
import 'package:watch_together/src/shell/library_link.dart';
|
import 'package:starsync/src/shell/library_link.dart';
|
||||||
import 'package:watch_together/src/shell/library_tab.dart';
|
import 'package:starsync/src/shell/library_tab.dart';
|
||||||
|
|
||||||
final now = DateTime.utc(2026, 10, 8, 12);
|
final now = DateTime.utc(2026, 10, 8, 12);
|
||||||
DateTime daysAgo(num d) => now.subtract(Duration(minutes: (d * 24 * 60).round()));
|
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/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/jellyfin/browse_scope.dart';
|
import 'package:starsync/src/jellyfin/browse_scope.dart';
|
||||||
import 'package:watch_together/src/jellyfin/details_panel.dart';
|
import 'package:starsync/src/jellyfin/details_panel.dart';
|
||||||
import 'package:watch_together/src/screens/home/media_picker.dart';
|
import 'package:starsync/src/screens/home/media_picker.dart';
|
||||||
import 'package:watch_together/src/shell/library_link.dart';
|
import 'package:starsync/src/shell/library_link.dart';
|
||||||
import 'package:watch_together/src/shell/library_tab.dart';
|
import 'package:starsync/src/shell/library_tab.dart';
|
||||||
|
|
||||||
import 'jellyfin_accounts_test.dart'
|
import 'jellyfin_accounts_test.dart'
|
||||||
show FakeJellyfin, arifuretaId, fixtureServerId, jellyfinFixture, linkedAccounts, season2Id;
|
show FakeJellyfin, arifuretaId, fixtureServerId, jellyfinFixture, linkedAccounts, season2Id;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/jellyfin/session_media.dart';
|
import 'package:starsync/src/jellyfin/session_media.dart';
|
||||||
import 'package:watch_together/src/jellyfin/track_prefs.dart';
|
import 'package:starsync/src/jellyfin/track_prefs.dart';
|
||||||
import 'package:watch_together/src/playback/media_marks.dart';
|
import 'package:starsync/src/playback/media_marks.dart';
|
||||||
|
|
||||||
Duration s(num secs) => Duration(milliseconds: (secs * 1000).round());
|
Duration s(num secs) => Duration(milliseconds: (secs * 1000).round());
|
||||||
Duration m(int min, [int sec = 0]) => Duration(minutes: min, seconds: sec);
|
Duration m(int min, [int sec = 0]) => Duration(minutes: min, seconds: sec);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/controls/frame.dart';
|
import 'package:starsync/src/controls/frame.dart';
|
||||||
import 'package:watch_together/src/controls/minimal/style.dart';
|
import 'package:starsync/src/controls/minimal/style.dart';
|
||||||
import 'package:watch_together/src/controls/minimal/timeline.dart';
|
import 'package:starsync/src/controls/minimal/timeline.dart';
|
||||||
import 'package:watch_together/src/controls/model/controls_prefs.dart';
|
import 'package:starsync/src/controls/model/controls_prefs.dart';
|
||||||
import 'package:watch_together/src/controls/volume.dart';
|
import 'package:starsync/src/controls/volume.dart';
|
||||||
import 'package:watch_together/src/playback/media_marks.dart';
|
import 'package:starsync/src/playback/media_marks.dart';
|
||||||
import 'package:watch_together/src/shell/settings_tab.dart';
|
import 'package:starsync/src/shell/settings_tab.dart';
|
||||||
import 'package:watch_together/src/state/settings.dart';
|
import 'package:starsync/src/state/settings.dart';
|
||||||
import 'package:watch_together/src/update/update_controller.dart';
|
import 'package:starsync/src/update/update_controller.dart';
|
||||||
import 'package:watch_together/src/widgets/app_menu.dart';
|
import 'package:starsync/src/widgets/app_menu.dart';
|
||||||
|
|
||||||
// The Minimal skin's pieces and the shared controls frame (never the
|
// The Minimal skin's pieces and the shared controls frame (never the
|
||||||
// session screen or a media_kit `Video`).
|
// session screen or a media_kit `Video`).
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart' show FeedMessage;
|
import 'package:sync_relay_core/core.dart' show FeedMessage;
|
||||||
import 'package:watch_together/src/session/notifier.dart';
|
import 'package:starsync/src/session/notifier.dart';
|
||||||
import 'package:watch_together/src/sfx/sfx.dart';
|
import 'package:starsync/src/sfx/sfx.dart';
|
||||||
|
|
||||||
final _at = DateTime.utc(2026, 1, 1);
|
final _at = DateTime.utc(2026, 1, 1);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/session/now_playing.dart';
|
import 'package:starsync/src/session/now_playing.dart';
|
||||||
import 'package:watch_together/src/session/now_playing_info.dart';
|
import 'package:starsync/src/session/now_playing_info.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('jellyfinTitle', () {
|
group('jellyfinTitle', () {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart' show JellyfinStreamType;
|
import 'package:sync_relay_core/core.dart' show JellyfinStreamType;
|
||||||
import 'package:watch_together/src/controls/control_menus.dart';
|
import 'package:starsync/src/controls/control_menus.dart';
|
||||||
import 'package:watch_together/src/controls/standard/seek_bar.dart';
|
import 'package:starsync/src/controls/standard/seek_bar.dart';
|
||||||
import 'package:watch_together/src/jellyfin/track_prefs.dart';
|
import 'package:starsync/src/jellyfin/track_prefs.dart';
|
||||||
import 'package:watch_together/src/playback/media_marks.dart';
|
import 'package:starsync/src/playback/media_marks.dart';
|
||||||
import 'package:watch_together/src/screens/session/player_controls.dart';
|
import 'package:starsync/src/screens/session/player_controls.dart';
|
||||||
|
|
||||||
// The control-bar pieces alone (never the session screen or a `Video`):
|
// The control-bar pieces alone (never the session screen or a `Video`):
|
||||||
// the Standard seek bar, the skip pill and the shared track / chapter
|
// 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/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/controls/frame.dart';
|
import 'package:starsync/src/controls/frame.dart';
|
||||||
import 'package:watch_together/src/controls/model/controls_prefs.dart';
|
import 'package:starsync/src/controls/model/controls_prefs.dart';
|
||||||
import 'package:watch_together/src/controls/player_settings.dart';
|
import 'package:starsync/src/controls/player_settings.dart';
|
||||||
import 'package:watch_together/src/jellyfin/track_prefs.dart';
|
import 'package:starsync/src/jellyfin/track_prefs.dart';
|
||||||
import 'package:watch_together/src/schedule/animeflow.dart';
|
import 'package:starsync/src/schedule/animeflow.dart';
|
||||||
import 'package:watch_together/src/shaders/shader_presets.dart';
|
import 'package:starsync/src/shaders/shader_presets.dart';
|
||||||
import 'package:watch_together/src/shell/avatar.dart';
|
import 'package:starsync/src/shell/avatar.dart';
|
||||||
import 'package:watch_together/src/shell/party_strip.dart';
|
import 'package:starsync/src/shell/party_strip.dart';
|
||||||
import 'package:watch_together/src/shell/settings_tab.dart';
|
import 'package:starsync/src/shell/settings_tab.dart';
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
import 'package:watch_together/src/state/settings.dart';
|
import 'package:starsync/src/state/settings.dart';
|
||||||
import 'package:watch_together/src/update/update_controller.dart';
|
import 'package:starsync/src/update/update_controller.dart';
|
||||||
|
|
||||||
// The player settings card (both skins, never a media_kit `Video`), the
|
// The player settings card (both skins, never a media_kit `Video`), the
|
||||||
// Settings pages, the host flag in the party UI and invite addresses.
|
// 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/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/controls/frame.dart';
|
import 'package:starsync/src/controls/frame.dart';
|
||||||
import 'package:watch_together/src/controls/minimal/style.dart';
|
import 'package:starsync/src/controls/minimal/style.dart';
|
||||||
import 'package:watch_together/src/controls/minimal/timeline.dart';
|
import 'package:starsync/src/controls/minimal/timeline.dart';
|
||||||
import 'package:watch_together/src/session/health.dart';
|
import 'package:starsync/src/session/health.dart';
|
||||||
import 'package:watch_together/src/session/now_playing.dart';
|
import 'package:starsync/src/session/now_playing.dart';
|
||||||
import 'package:watch_together/src/shell/now_playing_title.dart';
|
import 'package:starsync/src/shell/now_playing_title.dart';
|
||||||
import 'package:watch_together/src/shell/player_status.dart';
|
import 'package:starsync/src/shell/player_status.dart';
|
||||||
import 'package:watch_together/src/shell/session_player.dart';
|
import 'package:starsync/src/shell/session_player.dart';
|
||||||
import 'package:watch_together/src/shell/start_banner.dart';
|
import 'package:starsync/src/shell/start_banner.dart';
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
|
|
||||||
// The player's title block, start card and status pill (never a media_kit
|
// The player's title block, start card and status pill (never a media_kit
|
||||||
// `Video`).
|
// `Video`).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// the janitor become server state.
|
// the janitor become server state.
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.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,
|
MemberView member(String id,
|
||||||
{bool stale = false, bool out = false, Map<String, bool> jf = const {}, String name = ''}) =>
|
{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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/queue/queue_ui.dart';
|
import 'package:starsync/src/queue/queue_ui.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart';
|
import 'package:starsync/src/session/session_coordinator.dart';
|
||||||
import 'package:watch_together/src/shell/library_link.dart' show LinkPreview;
|
import 'package:starsync/src/shell/library_link.dart' show LinkPreview;
|
||||||
import 'package:watch_together/src/widgets/text_focus.dart';
|
import 'package:starsync/src/widgets/text_focus.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Up next popover: holds the keys, Ctrl+V adds with a preview row, + Add link opens a field',
|
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:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/jellyfin/session_media.dart';
|
import 'package:starsync/src/jellyfin/session_media.dart';
|
||||||
import 'package:watch_together/src/jellyfin/track_prefs.dart';
|
import 'package:starsync/src/jellyfin/track_prefs.dart';
|
||||||
import 'package:watch_together/src/playback/yt_dlp_app.dart';
|
import 'package:starsync/src/playback/yt_dlp_app.dart';
|
||||||
import 'package:watch_together/src/queue/queue_controller.dart' show WarmState;
|
import 'package:starsync/src/queue/queue_controller.dart' show WarmState;
|
||||||
import 'package:watch_together/src/screens/home/picked_media.dart';
|
import 'package:starsync/src/screens/home/picked_media.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart';
|
import 'package:starsync/src/session/session_coordinator.dart';
|
||||||
import 'package:watch_together/src/session/switch_trace.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
|
// 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
|
// 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:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.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;
|
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:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/jellyfin/track_prefs.dart';
|
import 'package:starsync/src/jellyfin/track_prefs.dart';
|
||||||
import 'package:watch_together/src/playback/yt_dlp_app.dart';
|
import 'package:starsync/src/playback/yt_dlp_app.dart';
|
||||||
import 'package:watch_together/src/screens/home/picked_media.dart';
|
import 'package:starsync/src/screens/home/picked_media.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart';
|
import 'package:starsync/src/session/session_coordinator.dart';
|
||||||
|
|
||||||
// The relay with real coordinators, real mpv and a dedicated server that
|
// 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
|
// 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:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/files/name_parser.dart';
|
import 'package:starsync/src/files/name_parser.dart';
|
||||||
|
|
||||||
void _expect(
|
void _expect(
|
||||||
String name,
|
String name,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import 'dart:io';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/schedule/anilist.dart';
|
import 'package:starsync/src/schedule/anilist.dart';
|
||||||
import 'package:watch_together/src/session/media_art.dart';
|
import 'package:starsync/src/session/media_art.dart';
|
||||||
|
|
||||||
import 'schedule_fake_server.dart';
|
import 'schedule_fake_server.dart';
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ void main() {
|
|||||||
cacheDir: () async => cache,
|
cacheDir: () async => cache,
|
||||||
delay: (d) async => delays.add(d),
|
delay: (d) async => delays.add(d),
|
||||||
now: () => now,
|
now: () => now,
|
||||||
userAgent: 'watch-together/test',
|
userAgent: 'Starsync/test',
|
||||||
);
|
);
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
@@ -73,7 +73,7 @@ void main() {
|
|||||||
expect(server.seen, hasLength(2));
|
expect(server.seen, hasLength(2));
|
||||||
final first = jsonDecode(server.seen.first.body) as Map;
|
final first = jsonDecode(server.seen.first.body) as Map;
|
||||||
expect(first['variables'], {'from': from.millisecondsSinceEpoch ~/ 1000 - 1, 'to': to.millisecondsSinceEpoch ~/ 1000, 'page': 1});
|
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]);
|
expect(s.airings.map((a) => a.anilistId), [1, 4]);
|
||||||
final a = s.airings.first;
|
final a = s.airings.first;
|
||||||
expect(a.title, 'Show');
|
expect(a.title, 'Show');
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/schedule/animeflow.dart';
|
import 'package:starsync/src/schedule/animeflow.dart';
|
||||||
|
|
||||||
import 'schedule_fake_server.dart';
|
import 'schedule_fake_server.dart';
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ void main() {
|
|||||||
final c = client();
|
final c = client();
|
||||||
final me = await c.me();
|
final me = await c.me();
|
||||||
expect(server.seen.single.headers.value('x-emby-token'), token);
|
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.userId, 'u1');
|
||||||
expect(me.name, 'Foxyz');
|
expect(me.name, 'Foxyz');
|
||||||
expect(me.scopes, {AnimeFlowScope.view, AnimeFlowScope.request});
|
expect(me.scopes, {AnimeFlowScope.view, AnimeFlowScope.request});
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/schedule/anilist.dart';
|
import 'package:starsync/src/schedule/anilist.dart';
|
||||||
import 'package:watch_together/src/schedule/animeflow.dart';
|
import 'package:starsync/src/schedule/animeflow.dart';
|
||||||
import 'package:watch_together/src/schedule/schedule_model.dart';
|
import 'package:starsync/src/schedule/schedule_model.dart';
|
||||||
|
|
||||||
import 'schedule_fake_server.dart';
|
import 'schedule_fake_server.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/schedule/anilist.dart';
|
import 'package:starsync/src/schedule/anilist.dart';
|
||||||
import 'package:watch_together/src/schedule/animeflow.dart';
|
import 'package:starsync/src/schedule/animeflow.dart';
|
||||||
import 'package:watch_together/src/schedule/schedule_model.dart';
|
import 'package:starsync/src/schedule/schedule_model.dart';
|
||||||
import 'package:watch_together/src/schedule/ui/schedule_actions.dart';
|
import 'package:starsync/src/schedule/ui/schedule_actions.dart';
|
||||||
import 'package:watch_together/src/schedule/ui/schedule_page.dart';
|
import 'package:starsync/src/schedule/ui/schedule_page.dart';
|
||||||
import 'package:watch_together/src/screens/home/picked_media.dart';
|
import 'package:starsync/src/screens/home/picked_media.dart';
|
||||||
|
|
||||||
import 'schedule_fake_server.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:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.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';
|
||||||
import 'package:watch_together/src/sync/state_diff.dart';
|
import 'package:starsync/src/sync/state_diff.dart';
|
||||||
|
|
||||||
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
|
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:hive_ce/hive.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/jellyfin/accounts.dart';
|
import 'package:starsync/src/jellyfin/accounts.dart';
|
||||||
import 'package:watch_together/src/screens/home/picked_media.dart';
|
import 'package:starsync/src/screens/home/picked_media.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart';
|
import 'package:starsync/src/session/session_coordinator.dart';
|
||||||
import 'package:watch_together/src/state/server_isolate.dart';
|
import 'package:starsync/src/state/server_isolate.dart';
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
|
|
||||||
Future<void> _poll(
|
Future<void> _poll(
|
||||||
FutureOr<bool> Function() cond,
|
FutureOr<bool> Function() cond,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
// IdleTracker and unreadyForNewMedia give way to auto sit-out;
|
// IdleTracker and unreadyForNewMedia give way to auto sit-out;
|
||||||
// linkDisplayName stays.
|
// linkDisplayName stays.
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/session/idle.dart';
|
import 'package:starsync/src/session/idle.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart';
|
import 'package:starsync/src/session/session_coordinator.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('IdleTracker', () {
|
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 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/sfx/sfx.dart';
|
import 'package:starsync/src/sfx/sfx.dart';
|
||||||
import 'package:watch_together/src/sfx/sound_bank.dart';
|
import 'package:starsync/src/sfx/sound_bank.dart';
|
||||||
import 'package:watch_together/src/sfx/synth.dart';
|
import 'package:starsync/src/sfx/synth.dart';
|
||||||
|
|
||||||
double _pitch(List<double> x, int from) {
|
double _pitch(List<double> x, int from) {
|
||||||
final c = <double>[];
|
final c = <double>[];
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:media_kit/media_kit.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';
|
||||||
import 'package:watch_together/src/shaders/shader_assets.dart';
|
import 'package:starsync/src/shaders/shader_assets.dart';
|
||||||
import 'package:watch_together/src/shaders/shader_presets.dart';
|
import 'package:starsync/src/shaders/shader_presets.dart';
|
||||||
import 'package:watch_together/src/state/settings.dart';
|
import 'package:starsync/src/state/settings.dart';
|
||||||
|
|
||||||
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
|
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:flutter_test/flutter_test.dart';
|
||||||
import 'package:hive_ce/hive.dart';
|
import 'package:hive_ce/hive.dart';
|
||||||
import 'package:watch_together/src/shaders/shader_assets.dart';
|
import 'package:starsync/src/shaders/shader_assets.dart';
|
||||||
import 'package:watch_together/src/shaders/shader_presets.dart';
|
import 'package:starsync/src/shaders/shader_presets.dart';
|
||||||
import 'package:watch_together/src/state/settings.dart';
|
import 'package:starsync/src/state/settings.dart';
|
||||||
|
|
||||||
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
|
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/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/screens/home/profile.dart';
|
import 'package:starsync/src/screens/home/profile.dart';
|
||||||
import 'package:watch_together/src/shell/app_shell.dart';
|
import 'package:starsync/src/shell/app_shell.dart';
|
||||||
import 'package:watch_together/src/shell/avatar.dart';
|
import 'package:starsync/src/shell/avatar.dart';
|
||||||
import 'package:watch_together/src/shell/motion.dart';
|
import 'package:starsync/src/shell/motion.dart';
|
||||||
import 'package:watch_together/src/shell/party_strip.dart';
|
import 'package:starsync/src/shell/party_strip.dart';
|
||||||
import 'package:watch_together/src/shell/profile_card.dart';
|
import 'package:starsync/src/shell/profile_card.dart';
|
||||||
import 'package:watch_together/src/shell/session_player.dart';
|
import 'package:starsync/src/shell/session_player.dart';
|
||||||
import 'package:watch_together/src/shell/settings_tab.dart' show SettingsTab;
|
import 'package:starsync/src/shell/settings_tab.dart' show SettingsTab;
|
||||||
import 'package:watch_together/src/shell/toasts.dart';
|
import 'package:starsync/src/shell/toasts.dart';
|
||||||
import 'package:watch_together/src/session/session_coordinator.dart' show ToastItem;
|
import 'package:starsync/src/session/session_coordinator.dart' show ToastItem;
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
import 'package:watch_together/src/update/update_controller.dart';
|
import 'package:starsync/src/update/update_controller.dart';
|
||||||
|
|
||||||
/// Stands in for media_kit's `Video` (which hangs flutter_test) and
|
/// Stands in for media_kit's `Video` (which hangs flutter_test) and
|
||||||
/// counts how often one is created.
|
/// counts how often one is created.
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'dart:io';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_soloud/flutter_soloud.dart';
|
import 'package:flutter_soloud/flutter_soloud.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/platform/audio/sound_engine.dart';
|
import 'package:starsync/src/platform/audio/sound_engine.dart';
|
||||||
import 'package:watch_together/src/sfx/sfx.dart';
|
import 'package:starsync/src/sfx/sfx.dart';
|
||||||
import 'package:watch_together/src/sfx/sound_bank.dart';
|
import 'package:starsync/src/sfx/sound_bank.dart';
|
||||||
import 'package:watch_together/src/sfx/synth_sfx.dart';
|
import 'package:starsync/src/sfx/synth_sfx.dart';
|
||||||
|
|
||||||
// Needs a real audio output: opens the device through flutter_soloud and
|
// Needs a real audio output: opens the device through flutter_soloud and
|
||||||
// plays every sound of every preset once, quietly (WT_SFX_VOLUME, default
|
// plays every sound of every preset once, quietly (WT_SFX_VOLUME, default
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:watch_together/src/platform/audio/sound_engine.dart';
|
import 'package:starsync/src/platform/audio/sound_engine.dart';
|
||||||
import 'package:watch_together/src/sfx/sfx.dart';
|
import 'package:starsync/src/sfx/sfx.dart';
|
||||||
import 'package:watch_together/src/sfx/sound_bank.dart';
|
import 'package:starsync/src/sfx/sound_bank.dart';
|
||||||
import 'package:watch_together/src/sfx/synth.dart';
|
import 'package:starsync/src/sfx/synth.dart';
|
||||||
import 'package:watch_together/src/sfx/synth_sfx.dart';
|
import 'package:starsync/src/sfx/synth_sfx.dart';
|
||||||
|
|
||||||
class _FakeBackend implements AudioBackend {
|
class _FakeBackend implements AudioBackend {
|
||||||
final bool failInit;
|
final bool failInit;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.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({
|
StateBroadcast _state({
|
||||||
int eventId = 0,
|
int eventId = 0,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.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 _port = 18080;
|
||||||
const _secret = 'e2e-secret';
|
const _secret = 'e2e-secret';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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() {
|
void main() {
|
||||||
// Regression: the old FocusNode-parent walk never met EditableText, so
|
// Regression: the old FocusNode-parent walk never met EditableText, so
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'dart:io';
|
|||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.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
|
// 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
|
// 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;
|
bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0;
|
||||||
|
|
||||||
// Builds the release archive exactly like tool/package_linux.sh:
|
// Builds the release archive exactly like tool/package_linux.sh:
|
||||||
// bundle copied into <stage>/watch-together-<ver>/, then
|
// bundle copied into <stage>/starsync-<ver>/ with the launcher by the
|
||||||
// `tar -czf out -C <stage> watch-together-<ver>`. [zip]: like
|
// old name (lib/src/update/bundle/watch_together, the shipped file), then
|
||||||
// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only).
|
// `tar -czf out -C <stage> starsync-<ver>`. [app]: the app binary's
|
||||||
|
// content.
|
||||||
Future<List<int>> _releaseTarball(Directory work,
|
Future<List<int>> _releaseTarball(Directory work,
|
||||||
{String version = '0.2.0',
|
{String version = '0.2.0', String app = 'binary-v2'}) async {
|
||||||
bool windows = false,
|
final stage = Directory('${work.path}/pkg-$version-lin');
|
||||||
bool bundle = true,
|
final folder = Directory('${stage.path}/starsync-$version');
|
||||||
bool zip = false}) async {
|
|
||||||
final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}');
|
|
||||||
final folder = Directory('${stage.path}/watch-together-$version');
|
|
||||||
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
|
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
|
||||||
await File('${folder.path}/data/flutter_assets/fresh.cfg')
|
await File('${folder.path}/data/flutter_assets/fresh.cfg')
|
||||||
.writeAsString('fresh-config');
|
.writeAsString('fresh-config');
|
||||||
if (windows) {
|
await File('${folder.path}/starsync').writeAsString(app);
|
||||||
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 Directory('${folder.path}/lib').create();
|
||||||
await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2');
|
await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2');
|
||||||
await Link('${folder.path}/lib/libapp.so').create('libapp.so.1');
|
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');
|
||||||
}
|
}
|
||||||
await File('${folder.path}/update.sh')
|
|
||||||
.writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n');
|
|
||||||
if (!Platform.isWindows) {
|
if (!Platform.isWindows) {
|
||||||
final r = await Process.run('chmod',
|
final r = await Process.run('chmod', [
|
||||||
['755', '${folder.path}/watch_together', '${folder.path}/update.sh']);
|
'755',
|
||||||
|
for (final f in ['starsync', 'update.sh', 'watch_together', 'watch-together-server'])
|
||||||
|
'${folder.path}/$f'
|
||||||
|
]);
|
||||||
expect(r.exitCode, 0);
|
expect(r.exitCode, 0);
|
||||||
}
|
}
|
||||||
}
|
final out = File('${work.path}/out-$version.tar.gz');
|
||||||
final out = File('${work.path}/out-$version.${zip ? 'zip' : 'tar.gz'}');
|
final r = await Process.run('tar', ['-czf', out.path, '-C', stage.path, 'starsync-$version']);
|
||||||
final r = await Process.run('tar', [
|
|
||||||
...zip ? ['-a', '-c', '-f'] : ['-czf'],
|
|
||||||
out.path,
|
|
||||||
'-C',
|
|
||||||
stage.path,
|
|
||||||
'watch-together-$version',
|
|
||||||
]);
|
|
||||||
expect(r.exitCode, 0, reason: '${r.stderr}');
|
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||||
return out.readAsBytes();
|
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 {
|
Future<Directory> _fakeRoot(Directory work, {bool windows = false}) async {
|
||||||
final root = Directory('${work.path}/install');
|
final root = Directory('${work.path}/install');
|
||||||
await Directory('${root.path}/data').create(recursive: true);
|
await Directory('${root.path}/data').create(recursive: true);
|
||||||
@@ -105,12 +95,12 @@ class _Server {
|
|||||||
req.response.write(jsonEncode({
|
req.response.write(jsonEncode({
|
||||||
'version': '0.2.0',
|
'version': '0.2.0',
|
||||||
'protocol': RelayConstants.protoVersion,
|
'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(),
|
'sha256': sha256.convert(tarball).toString(),
|
||||||
'notes': 'test build',
|
'notes': 'test build',
|
||||||
...manifestExtra,
|
...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++;
|
tarHits++;
|
||||||
req.response.contentLength = tarball.length;
|
req.response.contentLength = tarball.length;
|
||||||
req.response.add(tarball);
|
req.response.add(tarball);
|
||||||
@@ -145,12 +135,14 @@ void main() {
|
|||||||
test('check, download with progress, apply, restart same binary',
|
test('check, download with progress, apply, restart same binary',
|
||||||
() async {
|
() async {
|
||||||
final root = await _fakeRoot(work);
|
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>)>[];
|
final restarts = <(String, List<String>)>[];
|
||||||
launchArgs = const ['--some-flag'];
|
launchArgs = const ['--some-flag'];
|
||||||
final c = UpdateController(
|
final c = UpdateController(
|
||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
executablePath: '${root.path}/watch_together',
|
executablePath: '${root.path}/starsync',
|
||||||
restart: (exe, args) async => restarts.add((exe, args)));
|
restart: (exe, args) async => restarts.add((exe, args)));
|
||||||
await c.checkOnce();
|
await c.checkOnce();
|
||||||
expect(c.status.value.phase, UpdatePhase.available);
|
expect(c.status.value.phase, UpdatePhase.available);
|
||||||
@@ -164,13 +156,35 @@ void main() {
|
|||||||
await c.install();
|
await c.install();
|
||||||
expect(progress.last, 1.0);
|
expect(progress.last, 1.0);
|
||||||
expect(c.status.value.phase, UpdatePhase.restarting);
|
expect(c.status.value.phase, UpdatePhase.restarting);
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
expect(await File('${root.path}/starsync').readAsString(),
|
||||||
'binary-v2');
|
'binary-v2');
|
||||||
expect(_isExec('${root.path}/watch_together'), isTrue);
|
expect(_isExec('${root.path}/starsync'), isTrue);
|
||||||
expect(restarts.single.$1, '${root.path}/watch_together');
|
expect(restarts.single.$1, '${root.path}/starsync');
|
||||||
expect(restarts.single.$2, ['--some-flag']);
|
expect(restarts.single.$2, ['--some-flag']);
|
||||||
}, skip: _posixOnly);
|
}, 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',
|
test('newer protocol is a required update, cannot be dismissed',
|
||||||
() async {
|
() async {
|
||||||
server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1};
|
server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:sync_relay_core/core.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 _state({int eventId = 1, String? seekBy, int? seekSeq}) =>
|
||||||
StateBroadcast(
|
StateBroadcast(
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_server/server.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';
|
||||||
import 'package:watch_together/src/sync/state_diff.dart';
|
import 'package:starsync/src/sync/state_diff.dart';
|
||||||
|
|
||||||
const _mediaPath = '/home/foxyz/Videos/chiyo.mp4';
|
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:flutter_test/flutter_test.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart' show VideoState;
|
import 'package:media_kit_video/media_kit_video.dart' show VideoState;
|
||||||
import 'package:watch_together/src/controls/model/controls_prefs.dart';
|
import 'package:starsync/src/controls/model/controls_prefs.dart';
|
||||||
import 'package:watch_together/src/controls/frame.dart';
|
import 'package:starsync/src/controls/frame.dart';
|
||||||
import 'package:watch_together/src/shell/session_player.dart';
|
import 'package:starsync/src/shell/session_player.dart';
|
||||||
import 'package:watch_together/src/state/session_state.dart';
|
import 'package:starsync/src/state/session_state.dart';
|
||||||
import 'package:watch_together/src/state/settings.dart';
|
import 'package:starsync/src/state/settings.dart';
|
||||||
|
|
||||||
// The wheel anywhere on the full player flashes the volume readout, in
|
// The wheel anywhere on the full player flashes the volume readout, in
|
||||||
// both skins, windowed and fullscreen, with the controls up or hidden.
|
// 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:flutter_test/flutter_test.dart';
|
||||||
import 'package:media_kit/media_kit.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;
|
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
|
||||||
const _referer = 'https://www.bilibili.com/';
|
const _referer = 'https://www.bilibili.com/';
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Project-level configuration.
|
# Project-level configuration.
|
||||||
cmake_minimum_required(VERSION 3.14)
|
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 name of the executable created for the application. Change this to change
|
||||||
# the on-disk name of your application.
|
# 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
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
# versions of CMake.
|
# versions of CMake.
|
||||||
|
|||||||
@@ -89,13 +89,13 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
BLOCK "040904e4"
|
BLOCK "040904e4"
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "com.example" "\0"
|
VALUE "CompanyName", "foxyz" "\0"
|
||||||
VALUE "FileDescription", "watch_together" "\0"
|
VALUE "FileDescription", "Starsync" "\0"
|
||||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
VALUE "InternalName", "watch_together" "\0"
|
VALUE "InternalName", "Starsync" "\0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
VALUE "LegalCopyright", "Copyright (C) 2026 foxyz. All rights reserved." "\0"
|
||||||
VALUE "OriginalFilename", "watch_together.exe" "\0"
|
VALUE "OriginalFilename", "starsync.exe" "\0"
|
||||||
VALUE "ProductName", "watch_together" "\0"
|
VALUE "ProductName", "Starsync" "\0"
|
||||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace {
|
|||||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||||
#endif
|
#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.
|
/// Registry key for app theme preference.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Dedicated watch-together server (no display needed).
|
# Dedicated Starsync server (no display needed).
|
||||||
# Install: unpack the release tarball to /opt/watch-together (the unit only
|
# Install: unpack the release tarball to /opt/starsync (the unit only uses
|
||||||
# uses the watch-together-server binary in it), copy this file to
|
# the starsync-server binary in it), copy this file to
|
||||||
# /etc/systemd/system/, then `systemctl enable --now watch-together`.
|
# /etc/systemd/system/, then `systemctl enable --now starsync`.
|
||||||
|
# An install from before the rename (/opt/watch-together, unit
|
||||||
|
# watch-together, ExecStart .../watch-together-server) keeps working
|
||||||
|
# unchanged: the archive still ships watch-together-server as a launcher.
|
||||||
# Flags: --port <n> (default 8080), --secret <password> (empty = open
|
# Flags: --port <n> (default 8080), --secret <password> (empty = open
|
||||||
# server, anyone who can reach the port can join), --name, --media <file>,
|
# server, anyone who can reach the port can join), --name, --media <file>,
|
||||||
# --advertise-host <host>, --no-yt-dlp, --yt-dlp-dir <dir>, --config
|
# --advertise-host <host>, --no-yt-dlp, --yt-dlp-dir <dir>, --config
|
||||||
@@ -11,15 +14,15 @@
|
|||||||
# process lists are readable by other users.
|
# process lists are readable by other users.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=watch-together dedicated server
|
Description=Starsync dedicated server
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/opt/watch-together/watch-together-server --port 8080 --secret change-me
|
ExecStart=/opt/starsync/starsync-server --port 8080 --secret change-me
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
DynamicUser=true
|
DynamicUser=true
|
||||||
StateDirectory=watch-together
|
StateDirectory=starsync
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
+71
-19
@@ -9,7 +9,7 @@ Plain overview first: [HOW-IT-WORKS.md](HOW-IT-WORKS.md). Wire format:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging, Jellyfin client, updater, watched rule (`WatchSession`); `testing.dart` (test support for all three packages, not in `core.dart`: `FlakyProxy`) | pure Dart, no Flutter |
|
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging, Jellyfin client, updater, watched rule (`WatchSession`); `testing.dart` (test support for all three packages, not in `core.dart`: `FlakyProxy`) | pure Dart, no Flutter |
|
||||||
| `packages/server` | Relic HTTP + WebSocket server (`SyncServer`, `ServerHub`, `Session`, media endpoint, relay buffer, Jellyfin relay), the `serve()` entrypoint, `bin/server.dart` (dedicated server) | pure Dart, no Flutter |
|
| `packages/server` | Relic HTTP + WebSocket server (`SyncServer`, `ServerHub`, `Session`, media endpoint, relay buffer, Jellyfin relay), the `serve()` entrypoint, `bin/server.dart` (dedicated server) | pure Dart, no Flutter |
|
||||||
| `apps/desktop` | Flutter app: UI, `SyncClient`, `Connection`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, update UI (`UpdateController`) | ships with `watch-together-server` |
|
| `apps/desktop` | Flutter app: UI, `SyncClient`, `Connection`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, update UI (`UpdateController`) | ships with `starsync-server` |
|
||||||
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
|
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
|
||||||
| `deploy` | systemd unit for the dedicated server | |
|
| `deploy` | systemd unit for the dedicated server | |
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ size (`ServerIsolate.setFile`): the host's in-session File swap then sends a
|
|||||||
cancelled, solo starts again with the same media. Stop leaves without
|
cancelled, solo starts again with the same media. Stop leaves without
|
||||||
asking.
|
asking.
|
||||||
- **Dedicated server**: `bin/server.dart` compiled with `dart compile exe`
|
- **Dedicated server**: `bin/server.dart` compiled with `dart compile exe`
|
||||||
and shipped next to the app as `watch-together-server`. It parses flags
|
and shipped next to the app as `starsync-server`. It parses flags
|
||||||
(`--port`, `--secret`, `--name`, `--media`, `--advertise-host`,
|
(`--port`, `--secret`, `--name`, `--media`, `--advertise-host`,
|
||||||
`--jellyfin <file>`, `--[no-]yt-dlp`, `--yt-dlp-dir`, `--yt-dlp-cookies`, `--config`; default port 8080) into the same map and calls `serve`
|
`--jellyfin <file>`, `--[no-]yt-dlp`, `--yt-dlp-dir`, `--yt-dlp-cookies`, `--config`; default port 8080) into the same map and calls `serve`
|
||||||
(`--log-dir` only picks where its log files go, see "Logging")
|
(`--log-dir` only picks where its log files go, see "Logging")
|
||||||
@@ -1174,6 +1174,10 @@ handed on from there (the coordinator gets its `Notifier`, `Connection` and
|
|||||||
options, warm-up.
|
options, warm-up.
|
||||||
- `SystemNotifications`: an interface only; desktop shows none
|
- `SystemNotifications`: an interface only; desktop shows none
|
||||||
(`NoSystemNotifications`).
|
(`NoSystemNotifications`).
|
||||||
|
- `AppDirsPort` (`DesktopAppDirs`): where Hive and the logs live
|
||||||
|
(`AppDirs`, "Desktop UI"), and the copy of the data from before the
|
||||||
|
rename; resolved in `main` before the composition root exists, so not a
|
||||||
|
`Ports` field.
|
||||||
Still direct: the player (`PlaybackController`, media_kit, and its
|
Still direct: the player (`PlaybackController`, media_kit, and its
|
||||||
`AppYtDlp.instance.mpvRawOptions`), the updater's restart
|
`AppYtDlp.instance.mpvRawOptions`), the updater's restart
|
||||||
(`Process.start`), the session's own file reads for what it plays or
|
(`Process.start`), the session's own file reads for what it plays or
|
||||||
@@ -1503,11 +1507,38 @@ Settings persist in two Hive boxes: `connect_form` (last form, recent
|
|||||||
servers and links, the profile picture and colour; saved even when a join
|
servers and links, the profile picture and colour; saved even when a join
|
||||||
fails) and
|
fails) and
|
||||||
`settings`. Where Hive and the logs live is `AppDirs`
|
`settings`. Where Hive and the logs live is `AppDirs`
|
||||||
(`platform/app_dirs.dart`, resolved once in `main`): Hive's default folder
|
(`platform/ports/app_dirs.dart`, resolved once in `main` by the
|
||||||
(the documents folder) and `<app support>/logs`; with `STARSYNC_PROFILE`
|
`AppDirsPort`, `DesktopAppDirs` on the desktop): `<app support>/hive` and
|
||||||
(DEVELOPMENT "Dev loop") `<app support>/profiles/<name>/hive` and `logs`,
|
`<app support>/logs`; with `STARSYNC_PROFILE` (DEVELOPMENT "Dev loop")
|
||||||
and the window title gets ` [<name>]`. yt-dlp, shaders and caches stay
|
`<app support>/profiles/<name>/hive` and `logs`, and the window title gets
|
||||||
shared. Link media names are a web page's title (see "yt-dlp"), else
|
` [<name>]`. yt-dlp, shaders and caches stay shared. The app support
|
||||||
|
folder is path_provider's: Linux `~/.local/share/dev.foxyz.starsync` (the
|
||||||
|
application id), Windows `%APPDATA%\foxyz\Starsync` (the exe's company and
|
||||||
|
product name).
|
||||||
|
|
||||||
|
**Data from before the rename** (`platform/data_migration.dart`, run by
|
||||||
|
`DesktopAppDirs.resolve` before Hive opens and the log starts; its lines
|
||||||
|
are logged once the log is open): once per support folder (the marker
|
||||||
|
`<app support>/migration.json`: `migratedFrom`, `hiveFrom`, `boxes`,
|
||||||
|
`files`), the first existing old support folder (Linux
|
||||||
|
`moe.senkochan.watch_together`, then `watch_together`, next to the new one;
|
||||||
|
Windows `%APPDATA%\com.example\watch_together`) is copied in whole
|
||||||
|
(profiles, logs, shaders, yt-dlp; modes kept, so `cookies.txt` stays 0600;
|
||||||
|
symlinks as links), and the app's own Hive boxes (`appHiveBoxes`:
|
||||||
|
`settings`, `connect_form`, `files`, `jellyfin`, `track_prefs`, `.hive`
|
||||||
|
and `.lock`, by name only, never other apps' boxes there) from the
|
||||||
|
documents folder, where `Hive.initFlutter()` kept them up to 1.2.x, into
|
||||||
|
`<app support>/hive`. Copied, never moved (1.2.x keeps working), never
|
||||||
|
over a file already there. Lines name paths and counts, never contents.
|
||||||
|
The caches folder (thumbnails, the AniList schedule) is not copied: it
|
||||||
|
refills.
|
||||||
|
|
||||||
|
**Settings schema** (`state/settings_schema.dart`): the `settings` box
|
||||||
|
stores `schemaVersion`; `migrateSettings` (run when `AppSettings` loads)
|
||||||
|
applies the ordered `settingsMigrations` steps past the stored version up
|
||||||
|
to `settingsSchemaVersion`, stamping the version after each. 1 is the
|
||||||
|
layout of 1.2.x (a box without a version is at 1, no steps yet); a box
|
||||||
|
from a newer build is left alone. Link media names are a web page's title (see "yt-dlp"), else
|
||||||
derived from the URL path, never containing a query string.
|
derived from the URL path, never containing a query string.
|
||||||
|
|
||||||
Jellyfin on the desktop (`lib/src/jellyfin/`, over the core client):
|
Jellyfin on the desktop (`lib/src/jellyfin/`, over the core client):
|
||||||
@@ -1697,7 +1728,7 @@ dedicated server (`package:logging` loggers named by area: `relay`,
|
|||||||
`app`).
|
`app`).
|
||||||
|
|
||||||
- **Where**: the app writes `<app support>/logs/app-YYYY-MM-DD.log`
|
- **Where**: the app writes `<app support>/logs/app-YYYY-MM-DD.log`
|
||||||
(Linux `~/.local/share/moe.senkochan.watch_together/logs`; with a
|
(Linux `~/.local/share/dev.foxyz.starsync/logs`; with a
|
||||||
profile `<app support>/profiles/<name>/logs`, see `AppDirs` under
|
profile `<app support>/profiles/<name>/logs`, see `AppDirs` under
|
||||||
"Desktop UI"); its hosted server's records arrive over the isolate's
|
"Desktop UI"); its hosted server's records arrive over the isolate's
|
||||||
`events` port and go in the same file as `server.<logger>` (`relogLine`). The dedicated server
|
`events` port and go in the same file as `server.<logger>` (`relogLine`). The dedicated server
|
||||||
@@ -1809,13 +1840,19 @@ status.
|
|||||||
|
|
||||||
`assets/icon/icon.svg` is the source; `icon_<size>.png` and the Windows
|
`assets/icon/icon.svg` is the source; `icon_<size>.png` and the Windows
|
||||||
`app_icon.ico` are rendered from it (rsvg-convert, magick). The GTK
|
`app_icon.ico` are rendered from it (rsvg-convert, magick). The GTK
|
||||||
application id is `moe.senkochan.watch_together`, which Wayland uses as the
|
application id is `dev.foxyz.starsync` (also the X11 `WM_CLASS`, from
|
||||||
window's `app_id`; compositors take the icon from the `.desktop` file of
|
`g_set_prgname`), which Wayland uses as the window's `app_id`; compositors
|
||||||
that id. A tarball install has none, so on start a Linux release build
|
take the icon from the `.desktop` file of that id. A tarball install has
|
||||||
(`platform/desktop_entry.dart`, skipped in debug) writes
|
none, so on start a Linux release build (`platform/desktop_entry.dart`,
|
||||||
`~/.local/share/applications/moe.senkochan.watch_together.desktop` (Exec =
|
skipped in debug) writes
|
||||||
the running binary) and the icons into the hicolor theme, only when they
|
`~/.local/share/applications/dev.foxyz.starsync.desktop` (Exec = the
|
||||||
changed. The runner also sets the window icon from the bundle (X11).
|
running binary, `StartupWMClass` and `Icon` = the id) and the icons into
|
||||||
|
the hicolor theme, only when they changed, and deletes the entry and
|
||||||
|
icons 1.2.x wrote under its id `moe.senkochan.watch_together` (those exact
|
||||||
|
files only). The runner also sets the window icon from the bundle (X11).
|
||||||
|
Windows: the exe's version resource (`windows/runner/Runner.rc`) says
|
||||||
|
company `foxyz`, product `Starsync`; the window class is
|
||||||
|
`STARSYNC_WINDOW`.
|
||||||
|
|
||||||
## Updater
|
## Updater
|
||||||
|
|
||||||
@@ -1829,8 +1866,23 @@ folder (same filesystem, so moves are renames; the folder must be
|
|||||||
user-writable). Each top-level entry is moved aside and the new one moved in,
|
user-writable). Each top-level entry is moved aside and the new one moved in,
|
||||||
with rollback on failure (writing the running executable fails with ETXTBSY;
|
with rollback on failure (writing the running executable fails with ETXTBSY;
|
||||||
rename works). A packaged install is detected by `data/` and `lib/` next to
|
rename works). A packaged install is detected by `data/` and `lib/` next to
|
||||||
the binary (app or server, same folder); anything else exits with "update
|
the binary (app or server, same folder; Windows: `data/` and
|
||||||
manually". A manifest whose
|
`flutter_windows.dll`), whatever the binary is called; anything else exits
|
||||||
|
with "update manually". The archive must contain the running binary's name;
|
||||||
|
an old name (`watch_together`, `watch-together-server`, `.exe` on
|
||||||
|
Windows) is also satisfied by the new one (`starsync`, `starsync-server`).
|
||||||
|
**Names before the rename**: the archive (`starsync-<ver>-<platform>`,
|
||||||
|
top folder `starsync-<ver>/`) also carries those old names, because a
|
||||||
|
1.2.x updater checks that its own name is in the archive and then restarts
|
||||||
|
it, and VPS units and old `update.sh` copies run `watch-together-server`.
|
||||||
|
Linux: shell launchers (`lib/src/update/bundle/watch_together`,
|
||||||
|
`watch-together-server`, `exec` the new binary next to the resolved
|
||||||
|
script); Windows: copies of `starsync.exe` and `starsync-server.exe` (same
|
||||||
|
version resource, so the same data folder). After an update the app
|
||||||
|
restarts `relaunchExecutable(...)`: the running binary, or for an old name
|
||||||
|
the new one next to it, so a copy started by 1.2.x moves to `starsync.exe`
|
||||||
|
on its first own update. `update.sh` / `update.bat` run `starsync-server`,
|
||||||
|
else the old name. A manifest whose
|
||||||
protocol is newer than the local one is a required update. Windows apply
|
protocol is newer than the local one is a required update. Windows apply
|
||||||
renames in-use files to `*.old` (swept on next start); CI tests it against a
|
renames in-use files to `*.old` (swept on next start); CI tests it against a
|
||||||
running exe on NTFS. The manifest's `assets` maps `linux-x64` /
|
running exe on NTFS. The manifest's `assets` maps `linux-x64` /
|
||||||
@@ -1842,9 +1894,9 @@ About and the tooltip of the strip's Settings tab): a release's version,
|
|||||||
a test build's with its commit (`GIT_COMMIT`, short hash, `-dirty` with
|
a test build's with its commit (`GIT_COMMIT`, short hash, `-dirty` with
|
||||||
uncommitted changes, passed by `tool/package_linux.sh`), and for a dev
|
uncommitted changes, passed by `tool/package_linux.sh`), and for a dev
|
||||||
run the pubspec's version (`FLUTTER_BUILD_NAME`, which the Flutter tool
|
run the pubspec's version (`FLUTTER_BUILD_NAME`, which the Flutter tool
|
||||||
defines for every build and run) with "dev run". CLI: `watch-together-server --upgrade` (no display needed;
|
defines for every build and run) with "dev run". CLI: `starsync-server --upgrade` (no display needed;
|
||||||
the shipped `update.sh` / `update.bat` run it), `--check-update` prints only;
|
the shipped `update.sh` / `update.bat` run it), `--check-update` prints only;
|
||||||
it ends with "restart the server to run <version>". `watch_together --upgrade`
|
it ends with "restart the server to run <version>". `starsync --upgrade`
|
||||||
does the same from the app binary. In the app, `UpdateController`
|
does the same from the app binary. In the app, `UpdateController`
|
||||||
(`apps/desktop/lib/src/update/`) runs the same code for the update bar
|
(`apps/desktop/lib/src/update/`) runs the same code for the update bar
|
||||||
(`UpdateBar`, `screens/home/update_indicator.dart`): a band across the
|
(`UpdateBar`, `screens/home/update_indicator.dart`): a band across the
|
||||||
|
|||||||
+11
-5
@@ -21,7 +21,7 @@ cd apps/desktop && flutter pub get && flutter run -d linux
|
|||||||
cd apps/desktop && flutter build linux --release --dart-define=APP_VERSION=<x.y.z>
|
cd apps/desktop && flutter build linux --release --dart-define=APP_VERSION=<x.y.z>
|
||||||
tool/package_linux.sh [--skip-build] [--noupload] # dist/*.tar.gz
|
tool/package_linux.sh [--skip-build] [--noupload] # dist/*.tar.gz
|
||||||
powershell -File tool/package_windows.ps1 [-SkipBuild] # dist/*.zip (on Windows)
|
powershell -File tool/package_windows.ps1 [-SkipBuild] # dist/*.zip (on Windows)
|
||||||
cd packages/server && dart compile exe --define=APP_VERSION=<x.y.z> bin/server.dart -o watch-together-server
|
cd packages/server && dart compile exe --define=APP_VERSION=<x.y.z> bin/server.dart -o starsync-server
|
||||||
```
|
```
|
||||||
|
|
||||||
`APP_VERSION` defaults to `0.0.0-dev`; the packagers stamp it from the
|
`APP_VERSION` defaults to `0.0.0-dev`; the packagers stamp it from the
|
||||||
@@ -30,7 +30,13 @@ also passes `GIT_COMMIT` (what the app shows: "Updater" in
|
|||||||
[ARCHITECTURE.md](ARCHITECTURE.md)). Setting `DISCORD_WEBHOOK_URL` makes the packager post the
|
[ARCHITECTURE.md](ARCHITECTURE.md)). Setting `DISCORD_WEBHOOK_URL` makes the packager post the
|
||||||
tarball to Discord (never commit it). The packager always compiles the
|
tarball to Discord (never commit it). The packager always compiles the
|
||||||
dedicated server (with the Flutter SDK's `dart`) into the tarball as
|
dedicated server (with the Flutter SDK's `dart`) into the tarball as
|
||||||
`watch-together-server`, also with `--skip-build`.
|
`starsync-server`, also with `--skip-build`. The app binary is `starsync`
|
||||||
|
(`BINARY_NAME`; Linux application id `dev.foxyz.starsync`), the archive
|
||||||
|
`dist/starsync-<version>-linux-x64.tar.gz` / `-windows-x64.zip` with one
|
||||||
|
top folder `starsync-<version>/`; it also carries the names up to 1.2.x
|
||||||
|
(`watch_together`, `watch-together-server`: launchers on Linux, copies on
|
||||||
|
Windows; "Updater" in [ARCHITECTURE.md](ARCHITECTURE.md)). The Dart
|
||||||
|
package of the app is `starsync` (`package:starsync/...`).
|
||||||
|
|
||||||
## Dev loop
|
## Dev loop
|
||||||
|
|
||||||
@@ -224,8 +230,8 @@ unexpectedly` line just before it is expected. Without the app:
|
|||||||
|
|
||||||
After a session, read the log files rather than reproducing
|
After a session, read the log files rather than reproducing
|
||||||
("Logging" in [ARCHITECTURE.md](ARCHITECTURE.md) has what goes in them):
|
("Logging" in [ARCHITECTURE.md](ARCHITECTURE.md) has what goes in them):
|
||||||
the app's are `~/.local/share/moe.senkochan.watch_together/logs/app-*.log`
|
the app's are `~/.local/share/dev.foxyz.starsync/logs/app-*.log`
|
||||||
on Linux (on Windows under `%APPDATA%`; Settings → About → "Open logs
|
on Linux (on Windows `%APPDATA%\foxyz\Starsync\logs`; Settings → About → "Open logs
|
||||||
folder" finds it anywhere), with the hosted server's lines
|
folder" finds it anywhere), with the hosted server's lines
|
||||||
in the same file; a dedicated server prints its folder at start
|
in the same file; a dedicated server prints its folder at start
|
||||||
(`--log-dir`). Users send theirs with Settings → About → "Copy
|
(`--log-dir`). Users send theirs with Settings → About → "Copy
|
||||||
@@ -259,7 +265,7 @@ rasterizer: missing or blocked GPU drivers, `LIBGL_ALWAYS_SOFTWARE=1`);
|
|||||||
no usable EGL context: check `render env:` for an X11 session or a
|
no usable EGL context: check `render env:` for an X11 session or a
|
||||||
`GDK_BACKEND` / `FLUTTER_LINUX_RENDERER` override). Its reason is only on
|
`GDK_BACKEND` / `FLUTTER_LINUX_RENDERER` override). Its reason is only on
|
||||||
stderr: have the user start the app from a terminal
|
stderr: have the user start the app from a terminal
|
||||||
(`watch-together 2>&1 | grep VideoOutput`). `hwdec-current=no` on the
|
(`./starsync 2>&1 | grep VideoOutput`). `hwdec-current=no` on the
|
||||||
first-frame line means software decoding (no hwdec for that codec, or it
|
first-frame line means software decoding (no hwdec for that codec, or it
|
||||||
is off), not software rendering.
|
is off), not software rendering.
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ A five-minute tour. Details: [ARCHITECTURE.md](ARCHITECTURE.md), wire format:
|
|||||||
|
|
||||||
- **Host from the app**: the app you click Host in also runs the server.
|
- **Host from the app**: the app you click Host in also runs the server.
|
||||||
Simplest. The server lives as long as that app.
|
Simplest. The server lives as long as that app.
|
||||||
- **Dedicated server**: `watch-together-server`, a separate program in the
|
- **Dedicated server**: `starsync-server`, a separate program in the
|
||||||
release tarball, for an always-on box. See the README.
|
release tarball, for an always-on box. See the README.
|
||||||
|
|
||||||
Friends join either way with Join a friend in the strip at the top of the
|
Friends join either way with Join a friend in the strip at the top of the
|
||||||
|
|||||||
+14
-14
@@ -85,15 +85,6 @@ the automatic continuation and countdown, relay state), the device
|
|||||||
(client id, library and folders, match corrections, watch history,
|
(client id, library and folders, match corrections, watch history,
|
||||||
settings, notification prefs, what this app is sourcing) and the
|
settings, notification prefs, what this app is sourcing) and the
|
||||||
integrations (Jellyfin played/resume, AniList list).
|
integrations (Jellyfin played/resume, AniList list).
|
||||||
R. Rename (before step 1, so dev builds install side by side with 1.2.x):
|
|
||||||
app id `dev.foxyz.starsync`, binary, Windows company `foxyz`; Hive out
|
|
||||||
of `~/Documents` into the app's support folder, copied not moved;
|
|
||||||
a settings schema version with ordered migrations; `AppDirs` (today
|
|
||||||
Hive and logs, and `STARSYNC_PROFILE`) becomes a port and the default
|
|
||||||
moves next to the profiles;
|
|
||||||
the 1.3.0 archive still contains `watch_together(.exe)` so 1.2.x's
|
|
||||||
self-update restart works; the old `.desktop` entry removed; the
|
|
||||||
manifest kept at the old URL; VPS `ExecStart`/`update.sh` paths.
|
|
||||||
1. Connection + Identity + Presence; proto 16 opened (version ranges on
|
1. Connection + Identity + Presence; proto 16 opened (version ranges on
|
||||||
both sides, refusal frame, identity, grace, presence, relay resume,
|
both sides, refusal frame, identity, grace, presence, relay resume,
|
||||||
ids in the feed); later steps add fields, it is frozen at the 1.3.0
|
ids in the feed); later steps add fields, it is frozen at the 1.3.0
|
||||||
@@ -161,9 +152,14 @@ proto 16 together at the 1.3.0 release. Rooms are out (no room id).
|
|||||||
No Windows dev builds: restructure testing happens on Linux; Windows
|
No Windows dev builds: restructure testing happens on Linux; Windows
|
||||||
friends stay on 1.2.x until the 1.3.0 release (Windows-only fixes like
|
friends stay on 1.2.x until the 1.3.0 release (Windows-only fixes like
|
||||||
P30 reach them then).
|
P30 reach them then).
|
||||||
Rename to Starsync everywhere (repo, app id, data folder, updater URL
|
Left of the rename (app, binaries, archives and data folder are
|
||||||
migration) is its own step; app id `dev.foxyz.starsync`, Windows
|
Starsync): the repository (`Foxyz/sync_replay_project`) and with it the
|
||||||
company name `foxyz` (replacing Flutter's default `com.example`). Auto
|
manifest and asset URLs (a new manifest URL needs a release that reads
|
||||||
|
both first); the dedicated server's fallback folder
|
||||||
|
`~/.cache/watch-together`, the release token file
|
||||||
|
`~/.config/watch-together-release-token`, Jellyfin's client name "Watch
|
||||||
|
Together" (devices are listed under it) and `WatchTogetherApp`; dropping
|
||||||
|
the 1.2.x launchers from the archive once nobody updates from 1.2.x. Auto
|
||||||
sit-out: idle or minimised for a while sets you Away, activity sets you
|
sit-out: idle or minimised for a while sets you Away, activity sets you
|
||||||
In; it replaces the 10-minute auto-unready.
|
In; it replaces the 10-minute auto-unready.
|
||||||
|
|
||||||
@@ -338,8 +334,12 @@ over `wss://`/`https://` (`/health.tls`); apps use `wss://` when offered,
|
|||||||
5. **Ops**: Check shaders on Windows; the relay e2e "source drops
|
5. **Ops**: Check shaders on Windows; the relay e2e "source drops
|
||||||
uncleanly and resumes" fails about half the time under the full suite.
|
uncleanly and resumes" fails about half the time under the full suite.
|
||||||
|
|
||||||
6. **Windows: first real use.** CI builds and publishes
|
6. **Windows: first real use.** Check the rename at 1.3.0: a 1.2.x
|
||||||
`watch-together-<ver>-windows-x64.zip` (libmpv bundled) and runs the
|
install updates and restarts (`watch_together.exe`, a copy of
|
||||||
|
`starsync.exe`), the data in `%APPDATA%\foxyz\Starsync` came over from
|
||||||
|
`%APPDATA%\com.example\watch_together` and Documents, the next update
|
||||||
|
restarts `starsync.exe`. CI builds and publishes
|
||||||
|
`starsync-<ver>-windows-x64.zip` (libmpv bundled) and runs the
|
||||||
updater tests on NTFS; a friend has joined from it. Still unchecked:
|
updater tests on NTFS; a friend has joined from it. Still unchecked:
|
||||||
fullscreen controls, hosting (firewall prompt), in-app update from one
|
fullscreen controls, hosting (firewall prompt), in-app update from one
|
||||||
release to the next. The rest of the desktop tests are not in CI.
|
release to the next. The rest of the desktop tests are not in CI.
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ typedef DiagTarget = ({Uri base, String token, double offsetMs});
|
|||||||
/// saying so per connection; problem marks ([markProblem]) go either way.
|
/// saying so per connection; problem marks ([markProblem]) go either way.
|
||||||
class DiagUploader {
|
class DiagUploader {
|
||||||
final DiagTarget? Function() target;
|
final DiagTarget? Function() target;
|
||||||
/// The app and its version, e.g. `Watch Together 1.4.0 on linux`.
|
/// The app and its version, e.g. `Starsync 1.4.0 on linux`.
|
||||||
final String version;
|
final String version;
|
||||||
final int bufferBytes;
|
final int bufferBytes;
|
||||||
final int batchBytes;
|
final int batchBytes;
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import 'package:crypto/crypto.dart';
|
|||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
import 'logging.dart';
|
import 'logging.dart';
|
||||||
|
|
||||||
// The one updater for both programs: the app (`watch_together --upgrade`
|
// The one updater for both programs: the app (`starsync --upgrade`
|
||||||
// and the in-app update card, via UpdateController) and the dedicated
|
// and the in-app update card, via UpdateController) and the dedicated
|
||||||
// server (`watch-together-server --upgrade`). Both ship in the same
|
// server (`starsync-server --upgrade`). Both ship in the same
|
||||||
// release archive, so either one swaps the whole install folder: fetch
|
// release archive, so either one swaps the whole install folder: fetch
|
||||||
// manifest, download, verify sha256, apply over the live install.
|
// manifest, download, verify sha256, apply over the live install.
|
||||||
//
|
//
|
||||||
@@ -39,6 +39,41 @@ const appVersion = String.fromEnvironment('APP_VERSION',
|
|||||||
// Key into the manifest's `assets` map, e.g. linux-x64, windows-x64.
|
// Key into the manifest's `assets` map, e.g. linux-x64, windows-x64.
|
||||||
final updatePlatform = '${Platform.operatingSystem}-x64';
|
final updatePlatform = '${Platform.operatingSystem}-x64';
|
||||||
|
|
||||||
|
/// The app's binary in a release bundle (`.exe` on Windows).
|
||||||
|
const appBinaryName = 'starsync';
|
||||||
|
/// The dedicated server's binary next to it (`.exe` on Windows).
|
||||||
|
const serverBinaryName = 'starsync-server';
|
||||||
|
/// The names up to 1.2.x. The archive still carries launchers by these
|
||||||
|
/// names (tool/package_linux.sh, package_windows.ps1): a 1.2.x updater
|
||||||
|
/// checks that its own name is in the archive and restarts it, and VPS
|
||||||
|
/// units and `update.sh` copies run the old server name.
|
||||||
|
const legacyAppBinaryName = 'watch_together';
|
||||||
|
const legacyServerBinaryName = 'watch-together-server';
|
||||||
|
|
||||||
|
// For an old binary name, the new one (same extension); else null.
|
||||||
|
String? _renamedBinary(String name) {
|
||||||
|
final exe = name.toLowerCase().endsWith('.exe');
|
||||||
|
final stem = exe ? name.substring(0, name.length - 4) : name;
|
||||||
|
final renamed = switch (stem) {
|
||||||
|
legacyAppBinaryName => appBinaryName,
|
||||||
|
legacyServerBinaryName => serverBinaryName,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
return renamed == null ? null : '$renamed${exe ? name.substring(name.length - 4) : ''}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The binary to start after an update replaced [executablePath]: the
|
||||||
|
/// same one, except that an old name (a launcher, or on Windows a copy
|
||||||
|
/// of the new binary) gives way to the new binary next to it.
|
||||||
|
String relaunchExecutable(String executablePath) {
|
||||||
|
final norm = executablePath.replaceAll('\\', '/');
|
||||||
|
final cut = norm.lastIndexOf('/');
|
||||||
|
final renamed = _renamedBinary(norm.substring(cut + 1));
|
||||||
|
if (renamed == null) return executablePath;
|
||||||
|
final next = '${executablePath.substring(0, cut + 1)}$renamed';
|
||||||
|
return File(next).existsSync() ? next : executablePath;
|
||||||
|
}
|
||||||
|
|
||||||
const _stagingDir = '.update-staging';
|
const _stagingDir = '.update-staging';
|
||||||
const _backupDir = '.update-backup';
|
const _backupDir = '.update-backup';
|
||||||
const _downloadFile = '.update-download';
|
const _downloadFile = '.update-download';
|
||||||
@@ -303,7 +338,7 @@ List<String> _safeSegments(String name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Release archives wrap the bundle in one top-level folder
|
// Release archives wrap the bundle in one top-level folder
|
||||||
// (watch-together-<ver>/, see tool/package_linux.sh). Strip it; a flat
|
// (starsync-<ver>/, see tool/package_linux.sh). Strip it; a flat
|
||||||
// archive (data/, lib/ and the binary at the top) is used as is.
|
// archive (data/, lib/ and the binary at the top) is used as is.
|
||||||
int _wrapperDepth(List<List<String>> entries) {
|
int _wrapperDepth(List<List<String>> entries) {
|
||||||
final tops = entries.where((s) => s.isNotEmpty).map((s) => s.first).toSet();
|
final tops = entries.where((s) => s.isNotEmpty).map((s) => s.first).toSet();
|
||||||
@@ -414,7 +449,7 @@ Future<void> applyUpdate(File archive, Directory root,
|
|||||||
await _extract(await archive.readAsBytes(), stage, !win);
|
await _extract(await archive.readAsBytes(), stage, !win);
|
||||||
if (!_isBundle(stage.path, win) ||
|
if (!_isBundle(stage.path, win) ||
|
||||||
(executableName != null &&
|
(executableName != null &&
|
||||||
!await File('${stage.path}/$executableName').exists())) {
|
!await _hasBinary(stage.path, executableName))) {
|
||||||
throw const FormatException(
|
throw const FormatException(
|
||||||
'archive is not a release bundle (missing data/, lib/ or binary)');
|
'archive is not a release bundle (missing data/, lib/ or binary)');
|
||||||
}
|
}
|
||||||
@@ -428,6 +463,14 @@ Future<void> applyUpdate(File archive, Directory root,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The running binary's name, or for an old name its new one (an archive
|
||||||
|
// without the old launchers still updates an install started by one).
|
||||||
|
Future<bool> _hasBinary(String dir, String name) async {
|
||||||
|
if (await File('$dir/$name').exists()) return true;
|
||||||
|
final renamed = _renamedBinary(name);
|
||||||
|
return renamed != null && await File('$dir/$renamed').exists();
|
||||||
|
}
|
||||||
|
|
||||||
class _Moved {
|
class _Moved {
|
||||||
final String name;
|
final String name;
|
||||||
final bool hadOld;
|
final bool hadOld;
|
||||||
@@ -565,7 +608,7 @@ Future<int> runUpgrade(
|
|||||||
HttpClient? http,
|
HttpClient? http,
|
||||||
String? executablePath,
|
String? executablePath,
|
||||||
Directory? installRoot,
|
Directory? installRoot,
|
||||||
String program = 'watch-together',
|
String program = 'starsync',
|
||||||
String Function(String version)? restartNote,
|
String Function(String version)? restartNote,
|
||||||
bool checkOnly = false}) async {
|
bool checkOnly = false}) async {
|
||||||
stdout.writeln('$program $currentVersion (proto ${RelayConstants.protoVersion})');
|
stdout.writeln('$program $currentVersion (proto ${RelayConstants.protoVersion})');
|
||||||
|
|||||||
@@ -24,34 +24,48 @@ final _posixOnly = Platform.isWindows ? 'POSIX install layout' : null;
|
|||||||
bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0;
|
bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0;
|
||||||
|
|
||||||
// Builds the release archive exactly like tool/package_linux.sh:
|
// Builds the release archive exactly like tool/package_linux.sh:
|
||||||
// bundle copied into <stage>/watch-together-<ver>/, then
|
// bundle copied into <stage>/starsync-<ver>/, then
|
||||||
// `tar -czf out -C <stage> watch-together-<ver>`. [zip]: like
|
// `tar -czf out -C <stage> starsync-<ver>`. [zip]: like
|
||||||
// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only).
|
// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only).
|
||||||
|
// [legacy]: with the launchers by the names up to 1.2.x (Linux: the shell
|
||||||
|
// script of apps/desktop/lib/src/update/bundle/watch_together; Windows: a
|
||||||
|
// copy of the exe). [app]: the app binary's content (Linux).
|
||||||
Future<List<int>> _releaseTarball(Directory work,
|
Future<List<int>> _releaseTarball(Directory work,
|
||||||
{String version = '0.2.0',
|
{String version = '0.2.0',
|
||||||
bool windows = false,
|
bool windows = false,
|
||||||
bool bundle = true,
|
bool bundle = true,
|
||||||
bool zip = false}) async {
|
bool zip = false,
|
||||||
|
bool legacy = true,
|
||||||
|
String app = 'binary-v2'}) async {
|
||||||
final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}');
|
final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}');
|
||||||
final folder = Directory('${stage.path}/watch-together-$version');
|
final folder = Directory('${stage.path}/starsync-$version');
|
||||||
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
|
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
|
||||||
await File('${folder.path}/data/flutter_assets/fresh.cfg')
|
await File('${folder.path}/data/flutter_assets/fresh.cfg')
|
||||||
.writeAsString('fresh-config');
|
.writeAsString('fresh-config');
|
||||||
if (windows) {
|
if (windows) {
|
||||||
|
await File('${folder.path}/starsync.exe').writeAsString('binary-v2');
|
||||||
|
if (legacy) {
|
||||||
await File('${folder.path}/watch_together.exe').writeAsString('binary-v2');
|
await File('${folder.path}/watch_together.exe').writeAsString('binary-v2');
|
||||||
|
}
|
||||||
await File('${folder.path}/flutter_windows.dll').writeAsString('dll-v2');
|
await File('${folder.path}/flutter_windows.dll').writeAsString('dll-v2');
|
||||||
} else {
|
} else {
|
||||||
await File('${folder.path}/watch_together').writeAsString('binary-v2');
|
await File('${folder.path}/starsync').writeAsString(app);
|
||||||
if (bundle) {
|
if (bundle) {
|
||||||
await Directory('${folder.path}/lib').create();
|
await Directory('${folder.path}/lib').create();
|
||||||
await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2');
|
await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2');
|
||||||
await Link('${folder.path}/lib/libapp.so').create('libapp.so.1');
|
await Link('${folder.path}/lib/libapp.so').create('libapp.so.1');
|
||||||
}
|
}
|
||||||
await File('${folder.path}/update.sh')
|
await File('${folder.path}/update.sh')
|
||||||
.writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n');
|
.writeAsString('#!/bin/sh\nexec ./starsync-server --upgrade\n');
|
||||||
|
final exec = ['starsync', 'update.sh'];
|
||||||
|
if (legacy) {
|
||||||
|
await File('${folder.path}/watch_together').writeAsString(
|
||||||
|
'#!/bin/sh\nexec "\$(dirname "\$(readlink -f "\$0")")/starsync" "\$@"\n');
|
||||||
|
exec.add('watch_together');
|
||||||
|
}
|
||||||
if (!Platform.isWindows) {
|
if (!Platform.isWindows) {
|
||||||
final r = await Process.run('chmod',
|
final r = await Process.run(
|
||||||
['755', '${folder.path}/watch_together', '${folder.path}/update.sh']);
|
'chmod', ['755', for (final f in exec) '${folder.path}/$f']);
|
||||||
expect(r.exitCode, 0);
|
expect(r.exitCode, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,12 +75,13 @@ Future<List<int>> _releaseTarball(Directory work,
|
|||||||
out.path,
|
out.path,
|
||||||
'-C',
|
'-C',
|
||||||
stage.path,
|
stage.path,
|
||||||
'watch-together-$version',
|
'starsync-$version',
|
||||||
]);
|
]);
|
||||||
expect(r.exitCode, 0, reason: '${r.stderr}');
|
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||||
return out.readAsBytes();
|
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 {
|
Future<Directory> _fakeRoot(Directory work, {bool windows = false}) async {
|
||||||
final root = Directory('${work.path}/install');
|
final root = Directory('${work.path}/install');
|
||||||
await Directory('${root.path}/data').create(recursive: true);
|
await Directory('${root.path}/data').create(recursive: true);
|
||||||
@@ -111,12 +126,12 @@ class _Server {
|
|||||||
req.response.write(jsonEncode({
|
req.response.write(jsonEncode({
|
||||||
'version': '0.2.0',
|
'version': '0.2.0',
|
||||||
'protocol': RelayConstants.protoVersion,
|
'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(),
|
'sha256': sha256.convert(tarball).toString(),
|
||||||
'notes': 'test build',
|
'notes': 'test build',
|
||||||
...manifestExtra,
|
...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++;
|
tarHits++;
|
||||||
req.response.contentLength = tarball.length;
|
req.response.contentLength = tarball.length;
|
||||||
req.response.add(tarball);
|
req.response.add(tarball);
|
||||||
@@ -262,11 +277,12 @@ void main() {
|
|||||||
expect(rc, 0);
|
expect(rc, 0);
|
||||||
expect(server.tarHits, 1);
|
expect(server.tarHits, 1);
|
||||||
final p = root.path;
|
final p = root.path;
|
||||||
expect(await File('$p/watch_together').readAsString(), 'binary-v2');
|
expect(await File('$p/starsync').readAsString(), 'binary-v2');
|
||||||
expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
||||||
'fresh-config');
|
'fresh-config');
|
||||||
expect(Directory('$p/watch-together-0.2.0').existsSync(), isFalse);
|
expect(Directory('$p/starsync-0.2.0').existsSync(), isFalse);
|
||||||
expect(_isExec('$p/watch_together'), isTrue);
|
expect(_isExec('$p/starsync'), isTrue);
|
||||||
|
expect(_isExec('$p/watch_together'), isTrue, reason: 'the old name, a launcher');
|
||||||
expect(_isExec('$p/update.sh'), isTrue);
|
expect(_isExec('$p/update.sh'), isTrue);
|
||||||
expect(await Link('$p/lib/libapp.so').target(), 'libapp.so.1');
|
expect(await Link('$p/lib/libapp.so').target(), 'libapp.so.1');
|
||||||
expect(await File('$p/lib/libapp.so').readAsString(), 'lib-v2');
|
expect(await File('$p/lib/libapp.so').readAsString(), 'lib-v2');
|
||||||
@@ -310,7 +326,7 @@ void main() {
|
|||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
installRoot: root);
|
installRoot: root);
|
||||||
expect(rc, 0);
|
expect(rc, 0);
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
expect(await File('${root.path}/starsync').readAsString(),
|
||||||
'binary-v2');
|
'binary-v2');
|
||||||
}, skip: _posixOnly);
|
}, skip: _posixOnly);
|
||||||
|
|
||||||
@@ -358,6 +374,7 @@ void main() {
|
|||||||
expect(await File('$p/lib/libgone.so').readAsString(), 'lib-v1');
|
expect(await File('$p/lib/libgone.so').readAsString(), 'lib-v1');
|
||||||
expect(await File('$p/data/old.cfg').readAsString(), 'old-config');
|
expect(await File('$p/data/old.cfg').readAsString(), 'old-config');
|
||||||
expect(File('$p/update.sh').existsSync(), isFalse);
|
expect(File('$p/update.sh').existsSync(), isFalse);
|
||||||
|
expect(File('$p/starsync').existsSync(), isFalse);
|
||||||
expect(_leftovers(root), isEmpty);
|
expect(_leftovers(root), isEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +384,7 @@ void main() {
|
|||||||
'aside:data',
|
'aside:data',
|
||||||
'in:data',
|
'in:data',
|
||||||
'aside:lib',
|
'aside:lib',
|
||||||
|
'in:starsync',
|
||||||
'in:update.sh',
|
'in:update.sh',
|
||||||
'aside:watch_together',
|
'aside:watch_together',
|
||||||
'in:watch_together',
|
'in:watch_together',
|
||||||
@@ -423,12 +441,66 @@ void main() {
|
|||||||
expect(File('${root.path}/data/thing.dll.old').existsSync(), isFalse);
|
expect(File('${root.path}/data/thing.dll.old').existsSync(), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A 1.2.x updater checks its own name is in the archive, applies, then
|
||||||
|
// restarts that name: the launcher must start the new binary.
|
||||||
|
test('1.2.x install: the old name is in the archive and starts the new binary',
|
||||||
|
() async {
|
||||||
|
final root = await _fakeRoot(work);
|
||||||
|
final f = File('${work.path}/script.tar.gz')
|
||||||
|
..writeAsBytesSync(await _releaseTarball(work,
|
||||||
|
app: '#!/bin/sh\necho "new binary: \$*"\n'));
|
||||||
|
await applyUpdate(f, root,
|
||||||
|
windows: false, executableName: legacyAppBinaryName);
|
||||||
|
final r = await Process.run('${root.path}/watch_together', ['--x']);
|
||||||
|
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||||
|
expect('${r.stdout}'.trim(), 'new binary: --x');
|
||||||
|
// This build's own restart goes to the new binary directly.
|
||||||
|
expect(relaunchExecutable('${root.path}/watch_together'),
|
||||||
|
'${root.path}/starsync');
|
||||||
|
}, skip: _posixOnly);
|
||||||
|
|
||||||
|
test('started by an old name, an archive without it still applies',
|
||||||
|
() async {
|
||||||
|
final root = await _fakeRoot(work);
|
||||||
|
final f = File('${work.path}/plain.tar.gz')
|
||||||
|
..writeAsBytesSync(await _releaseTarball(work, legacy: false));
|
||||||
|
await applyUpdate(f, root,
|
||||||
|
windows: false, executableName: legacyAppBinaryName);
|
||||||
|
expect(await File('${root.path}/starsync').readAsString(), 'binary-v2');
|
||||||
|
// Unknown names still need their binary in the archive.
|
||||||
|
final again = File('${work.path}/plain2.tar.gz')
|
||||||
|
..writeAsBytesSync(await _releaseTarball(work, version: '0.3.0', legacy: false));
|
||||||
|
await expectLater(
|
||||||
|
applyUpdate(again, root, windows: false, executableName: 'other'),
|
||||||
|
throwsFormatException);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('relaunchExecutable maps old names to the new binary next to them',
|
||||||
|
() async {
|
||||||
|
final dir = (await work.createTemp('r')).path;
|
||||||
|
for (final n in ['starsync', 'starsync.exe', 'starsync-server']) {
|
||||||
|
await File('$dir/$n').writeAsString('x');
|
||||||
|
}
|
||||||
|
expect(relaunchExecutable('$dir/watch_together'), '$dir/starsync');
|
||||||
|
expect(relaunchExecutable('$dir/watch_together.exe'), '$dir/starsync.exe');
|
||||||
|
expect(relaunchExecutable('$dir/watch-together-server'),
|
||||||
|
'$dir/starsync-server');
|
||||||
|
expect(relaunchExecutable('$dir/starsync'), '$dir/starsync');
|
||||||
|
expect(relaunchExecutable('$dir/other'), '$dir/other');
|
||||||
|
// No new binary next to it: the old name stays.
|
||||||
|
expect(relaunchExecutable('${work.path}/watch_together'),
|
||||||
|
'${work.path}/watch_together');
|
||||||
|
expect(relaunchExecutable(r'C:\x\watch_together.exe'),
|
||||||
|
r'C:\x\watch_together.exe');
|
||||||
|
});
|
||||||
|
|
||||||
// Windows strategy exercised on Linux: proves the bookkeeping, not the
|
// Windows strategy exercised on Linux: proves the bookkeeping, not the
|
||||||
// Windows file-locking semantics (no Windows runner yet).
|
// Windows file-locking semantics (no Windows runner yet).
|
||||||
test('windows-style swap applies and parks old files', () async {
|
test('windows-style swap applies and parks old files', () async {
|
||||||
final root = await _fakeRoot(work, windows: true);
|
final root = await _fakeRoot(work, windows: true);
|
||||||
await applyUpdate(await archive(windows: true), root, windows: true);
|
await applyUpdate(await archive(windows: true), root, windows: true);
|
||||||
final p = root.path;
|
final p = root.path;
|
||||||
|
expect(await File('$p/starsync.exe').readAsString(), 'binary-v2');
|
||||||
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v2');
|
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v2');
|
||||||
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v2');
|
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v2');
|
||||||
expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
||||||
@@ -452,6 +524,7 @@ void main() {
|
|||||||
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v1');
|
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v1');
|
||||||
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v1');
|
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v1');
|
||||||
expect(File('$p/data/flutter_assets/fresh.cfg').existsSync(), isFalse);
|
expect(File('$p/data/flutter_assets/fresh.cfg').existsSync(), isFalse);
|
||||||
|
expect(File('$p/starsync.exe').existsSync(), isFalse);
|
||||||
expect(File('$p/flutter_windows.dll.old').existsSync(), isFalse);
|
expect(File('$p/flutter_windows.dll.old').existsSync(), isFalse);
|
||||||
expect(_leftovers(root), isEmpty);
|
expect(_leftovers(root), isEmpty);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import 'package:sync_relay_core/core.dart';
|
|||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
|
|
||||||
/// Dedicated server: `dart compile exe` of this file is shipped as
|
/// Dedicated server: `dart compile exe` of this file is shipped as
|
||||||
/// `watch-together-server`. Runs [serve] until SIGINT or SIGTERM.
|
/// `starsync-server` (also started by the `watch-together-server`
|
||||||
|
/// launcher). Runs [serve] until SIGINT or SIGTERM.
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
final parser = ArgParser()
|
final parser = ArgParser()
|
||||||
..addOption('port', abbr: 'p', defaultsTo: '8080', help: 'TCP port to listen on (all IPv4 interfaces).')
|
..addOption('port', abbr: 'p', defaultsTo: '8080', help: 'TCP port to listen on (all IPv4 interfaces).')
|
||||||
@@ -50,12 +51,12 @@ Future<void> main(List<String> args) async {
|
|||||||
}
|
}
|
||||||
if (parsed['help'] as bool) {
|
if (parsed['help'] as bool) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('watch-together-server $appVersion (proto ${RelayConstants.protoVersion})\n\n${parser.usage}');
|
print('starsync-server $appVersion (proto ${RelayConstants.protoVersion})\n\n${parser.usage}');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (parsed['upgrade'] as bool || parsed['check-update'] as bool) {
|
if (parsed['upgrade'] as bool || parsed['check-update'] as bool) {
|
||||||
exitCode = await runUpgrade(
|
exitCode = await runUpgrade(
|
||||||
program: 'watch-together-server',
|
program: 'starsync-server',
|
||||||
checkOnly: !(parsed['upgrade'] as bool),
|
checkOnly: !(parsed['upgrade'] as bool),
|
||||||
restartNote: (v) => 'updated to $v — restart the server to run $v.');
|
restartNote: (v) => 'updated to $v — restart the server to run $v.');
|
||||||
return;
|
return;
|
||||||
@@ -67,7 +68,7 @@ Future<void> main(List<String> args) async {
|
|||||||
(configPath != null ? '${File(configPath).absolute.parent.path}${Platform.pathSeparator}logs' : serverDataDir('logs'));
|
(configPath != null ? '${File(configPath).absolute.parent.path}${Platform.pathSeparator}logs' : serverDataDir('logs'));
|
||||||
final files = startLogFiles(logDir, prefix: 'server');
|
final files = startLogFiles(logDir, prefix: 'server');
|
||||||
if (files != null) stdout.writeln('logs: ${files.dir}');
|
if (files != null) stdout.writeln('logs: ${files.dir}');
|
||||||
pkg_logging.Logger('server').info('watch-together-server $appVersion (proto ${RelayConstants.protoVersion}) '
|
pkg_logging.Logger('server').info('starsync-server $appVersion (proto ${RelayConstants.protoVersion}) '
|
||||||
'on ${Platform.operatingSystem} ${Platform.operatingSystemVersion}, pid $pid');
|
'on ${Platform.operatingSystem} ${Platform.operatingSystemVersion}, pid $pid');
|
||||||
AppConfig config;
|
AppConfig config;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ export VERSION="$NEXT-dev.$N"
|
|||||||
echo "building $VERSION (last published: ${LAST:-none})"
|
echo "building $VERSION (last published: ${LAST:-none})"
|
||||||
|
|
||||||
"$ROOT/tool/package_linux.sh" --no-upload
|
"$ROOT/tool/package_linux.sh" --no-upload
|
||||||
TARBALL="$ROOT/dist/watch-together-$VERSION-linux-x64.tar.gz"
|
TARBALL="$ROOT/dist/starsync-$VERSION-linux-x64.tar.gz"
|
||||||
[[ -f "$TARBALL" ]] || { echo "error: $TARBALL missing" >&2; exit 1; }
|
[[ -f "$TARBALL" ]] || { echo "error: $TARBALL missing" >&2; exit 1; }
|
||||||
|
|
||||||
NOTES="$(git -C "$ROOT" log -1 --format=%s)"
|
NOTES="$(git -C "$ROOT" log -1 --format=%s)"
|
||||||
@@ -90,5 +90,5 @@ cat <<EOF
|
|||||||
|
|
||||||
Published $VERSION. Dev builds pick it up with their next update check.
|
Published $VERSION. Dev builds pick it up with their next update check.
|
||||||
A stable install moves onto test builds once: with the app open, run (bash)
|
A stable install moves onto test builds once: with the app open, run (bash)
|
||||||
p=\$(pgrep -xo watch_together) && d=\$(dirname "\$(readlink /proc/\$p/exe)") && t=\$(mktemp -d) && curl -fsSL $BASE_URL/$(basename "$TARBALL") | tar -xz -C "\$t" --strip-components=1 && cp -rf --remove-destination "\$t"/. "\$d"/ && rm -rf "\$t" && echo "Done: restart the app." || echo "Failed: is the app open?"
|
p=\$(pgrep -xo 'starsync|watch_together') && d=\$(dirname "\$(readlink /proc/\$p/exe)") && t=\$(mktemp -d) && curl -fsSL $BASE_URL/$(basename "$TARBALL") | tar -xz -C "\$t" --strip-components=1 && cp -rf --remove-destination "\$t"/. "\$d"/ && rm -rf "\$t" && echo "Done: restart the app." || echo "Failed: is the app open?"
|
||||||
EOF
|
EOF
|
||||||
+16
-11
@@ -2,13 +2,16 @@
|
|||||||
# Package the Linux release bundle into a versioned tar.gz and optionally
|
# Package the Linux release bundle into a versioned tar.gz and optionally
|
||||||
# upload it to Discord.
|
# upload it to Discord.
|
||||||
# Usage: tool/package_linux.sh [--skip-build] [--noupload|--no-upload]
|
# Usage: tool/package_linux.sh [--skip-build] [--noupload|--no-upload]
|
||||||
# Output: dist/watch-together-<version>-linux-x64.tar.gz containing one
|
# Output: dist/starsync-<version>-linux-x64.tar.gz containing one
|
||||||
# top-level folder with the executable, lib/ and data/ as built, plus
|
# top-level folder starsync-<version>/ with the executable `starsync`, lib/
|
||||||
# update.sh (runs watch-together-server --upgrade) and watch-together-server
|
# and data/ as built, plus update.sh (runs starsync-server --upgrade) and
|
||||||
# (the dedicated server, `dart compile exe` of packages/server/bin/server.dart,
|
# starsync-server (the dedicated server, `dart compile exe` of
|
||||||
# built every run, also with --skip-build; both binaries are stamped with
|
# packages/server/bin/server.dart, built every run, also with --skip-build;
|
||||||
# APP_VERSION from the app's pubspec). The updater strips
|
# both binaries are stamped with APP_VERSION from the app's pubspec), and
|
||||||
# that top-level folder — keep the layout in sync with packages/core/test/updater_test.dart.
|
# the launchers `watch_together` and `watch-together-server` (the names up
|
||||||
|
# to 1.2.x: their updater restarts `watch_together`, VPS units run the old
|
||||||
|
# server name). The updater strips that top-level folder — keep the layout
|
||||||
|
# in sync with packages/core/test/updater_test.dart.
|
||||||
# Upload: set DISCORD_WEBHOOK_URL in the environment (never commit it).
|
# Upload: set DISCORD_WEBHOOK_URL in the environment (never commit it).
|
||||||
# The tarball must stay under Discord's upload limit (25 MB free tier).
|
# The tarball must stay under Discord's upload limit (25 MB free tier).
|
||||||
# The upload posts a Components V2 card (flag 1<<15): version header,
|
# The upload posts a Components V2 card (flag 1<<15): version header,
|
||||||
@@ -28,7 +31,7 @@ done
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
APP="$ROOT/apps/desktop"
|
APP="$ROOT/apps/desktop"
|
||||||
NAME="watch-together"
|
NAME="starsync"
|
||||||
ARCH="x64"
|
ARCH="x64"
|
||||||
|
|
||||||
# VERSION in the environment overrides it (tool/dev_release.sh).
|
# VERSION in the environment overrides it (tool/dev_release.sh).
|
||||||
@@ -56,7 +59,9 @@ TARBALL="$DIST/$FOLDER-linux-$ARCH.tar.gz"
|
|||||||
rm -rf "$STAGE"
|
rm -rf "$STAGE"
|
||||||
mkdir -p "$STAGE/$FOLDER" "$DIST"
|
mkdir -p "$STAGE/$FOLDER" "$DIST"
|
||||||
cp -r "$BUNDLE/." "$STAGE/$FOLDER/"
|
cp -r "$BUNDLE/." "$STAGE/$FOLDER/"
|
||||||
install -m 755 "$APP/lib/src/update/bundle/update.sh" "$STAGE/$FOLDER/update.sh"
|
for f in update.sh watch_together watch-together-server; do
|
||||||
|
install -m 755 "$APP/lib/src/update/bundle/$f" "$STAGE/$FOLDER/$f"
|
||||||
|
done
|
||||||
# Prefer the dart bundled with the Flutter SDK that builds the app.
|
# Prefer the dart bundled with the Flutter SDK that builds the app.
|
||||||
DART="dart"
|
DART="dart"
|
||||||
if FLUTTER="$(command -v flutter)"; then
|
if FLUTTER="$(command -v flutter)"; then
|
||||||
@@ -64,7 +69,7 @@ if FLUTTER="$(command -v flutter)"; then
|
|||||||
[[ -x "$CANDIDATE" ]] && DART="$CANDIDATE"
|
[[ -x "$CANDIDATE" ]] && DART="$CANDIDATE"
|
||||||
fi
|
fi
|
||||||
(cd "$ROOT/packages/server" && "$DART" pub get >/dev/null &&
|
(cd "$ROOT/packages/server" && "$DART" pub get >/dev/null &&
|
||||||
"$DART" compile exe --define=APP_VERSION="$VERSION" bin/server.dart -o "$STAGE/$FOLDER/watch-together-server")
|
"$DART" compile exe --define=APP_VERSION="$VERSION" bin/server.dart -o "$STAGE/$FOLDER/starsync-server")
|
||||||
tar -czf "$TARBALL" -C "$STAGE" "$FOLDER"
|
tar -czf "$TARBALL" -C "$STAGE" "$FOLDER"
|
||||||
rm -rf "$STAGE"
|
rm -rf "$STAGE"
|
||||||
ls -la "$TARBALL"
|
ls -la "$TARBALL"
|
||||||
@@ -119,7 +124,7 @@ payload = {
|
|||||||
'accent_color': 5793266,
|
'accent_color': 5793266,
|
||||||
'components': [
|
'components': [
|
||||||
{'type': 10, 'content':
|
{'type': 10, 'content':
|
||||||
f'# 📦 watch-together `{ver}`\n'
|
f'# 📦 Starsync `{ver}`\n'
|
||||||
f'-# linux · {arch} · release · {size_mb:.1f} MB'},
|
f'-# linux · {arch} · release · {size_mb:.1f} MB'},
|
||||||
{'type': 10, 'content': f'`{commit[:140]}`'},
|
{'type': 10, 'content': f'`{commit[:140]}`'},
|
||||||
{'type': 10, 'content': block[:1500]},
|
{'type': 10, 'content': block[:1500]},
|
||||||
|
|||||||
+14
-10
@@ -1,19 +1,21 @@
|
|||||||
# Package the Windows release bundle into a versioned zip.
|
# Package the Windows release bundle into a versioned zip.
|
||||||
# Usage: powershell -File tool/package_windows.ps1 [-SkipBuild]
|
# Usage: powershell -File tool/package_windows.ps1 [-SkipBuild]
|
||||||
# Output: dist/watch-together-<version>-windows-x64.zip containing one
|
# Output: dist/starsync-<version>-windows-x64.zip containing one
|
||||||
# top-level folder with watch_together.exe, its DLLs and data/ as built,
|
# top-level folder starsync-<version>/ with starsync.exe, its DLLs and
|
||||||
# plus update.bat (runs watch-together-server.exe --upgrade) and
|
# data/ as built, plus update.bat (runs starsync-server.exe --upgrade) and
|
||||||
# watch-together-server.exe (dedicated server, `dart compile exe` of
|
# starsync-server.exe (dedicated server, `dart compile exe` of
|
||||||
# packages/server/bin/server.dart; both binaries stamped with APP_VERSION).
|
# packages/server/bin/server.dart; both binaries stamped with APP_VERSION),
|
||||||
# Same layout as tool/package_linux.sh; the updater strips the top-level
|
# and copies of both under the names up to 1.2.x (watch_together.exe,
|
||||||
# folder.
|
# watch-together-server.exe: a 1.2.x updater checks for and restarts its
|
||||||
|
# own name). Same layout as tool/package_linux.sh; the updater strips the
|
||||||
|
# top-level folder.
|
||||||
# Needs: flutter on PATH, tar.exe (ships with Windows 10+, writes zip).
|
# Needs: flutter on PATH, tar.exe (ships with Windows 10+, writes zip).
|
||||||
param([switch]$SkipBuild)
|
param([switch]$SkipBuild)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
$Root = Split-Path -Parent $PSScriptRoot
|
$Root = Split-Path -Parent $PSScriptRoot
|
||||||
$App = Join-Path $Root 'apps/desktop'
|
$App = Join-Path $Root 'apps/desktop'
|
||||||
$Name = 'watch-together'
|
$Name = 'starsync'
|
||||||
$Arch = 'x64'
|
$Arch = 'x64'
|
||||||
|
|
||||||
$Version = (Select-String -Path "$App/pubspec.yaml" -Pattern '^version: ([0-9.]+)').Matches[0].Groups[1].Value
|
$Version = (Select-String -Path "$App/pubspec.yaml" -Pattern '^version: ([0-9.]+)').Matches[0].Groups[1].Value
|
||||||
@@ -58,7 +60,7 @@ if (-not $SkipBuild) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$Bundle = Join-Path $App "build/windows/$Arch/runner/Release"
|
$Bundle = Join-Path $App "build/windows/$Arch/runner/Release"
|
||||||
if (-not (Test-Path "$Bundle/watch_together.exe")) { throw "bundle missing (build first): $Bundle" }
|
if (-not (Test-Path "$Bundle/starsync.exe")) { throw "bundle missing (build first): $Bundle" }
|
||||||
|
|
||||||
$Stage = Join-Path $App 'build/package-stage'
|
$Stage = Join-Path $App 'build/package-stage'
|
||||||
$Dist = Join-Path $Root 'dist'
|
$Dist = Join-Path $Root 'dist'
|
||||||
@@ -68,12 +70,14 @@ if (Test-Path $Stage) { Remove-Item -Recurse -Force $Stage }
|
|||||||
New-Item -ItemType Directory -Force "$Stage/$Folder", $Dist | Out-Null
|
New-Item -ItemType Directory -Force "$Stage/$Folder", $Dist | Out-Null
|
||||||
Copy-Item -Recurse "$Bundle/*" "$Stage/$Folder/"
|
Copy-Item -Recurse "$Bundle/*" "$Stage/$Folder/"
|
||||||
Copy-Item "$App/lib/src/update/bundle/update.bat" "$Stage/$Folder/update.bat"
|
Copy-Item "$App/lib/src/update/bundle/update.bat" "$Stage/$Folder/update.bat"
|
||||||
|
Copy-Item "$Stage/$Folder/starsync.exe" "$Stage/$Folder/watch_together.exe"
|
||||||
|
|
||||||
Push-Location (Join-Path $Root 'packages/server')
|
Push-Location (Join-Path $Root 'packages/server')
|
||||||
try {
|
try {
|
||||||
Invoke-Checked 'dart pub get' { dart pub get | Out-Null }
|
Invoke-Checked 'dart pub get' { dart pub get | Out-Null }
|
||||||
Invoke-Checked 'dart compile exe' { dart compile exe "--define=APP_VERSION=$Version" bin/server.dart -o "$Stage/$Folder/watch-together-server.exe" }
|
Invoke-Checked 'dart compile exe' { dart compile exe "--define=APP_VERSION=$Version" bin/server.dart -o "$Stage/$Folder/starsync-server.exe" }
|
||||||
} finally { Pop-Location }
|
} finally { Pop-Location }
|
||||||
|
Copy-Item "$Stage/$Folder/starsync-server.exe" "$Stage/$Folder/watch-together-server.exe"
|
||||||
|
|
||||||
if (Test-Path $Zip) { Remove-Item -Force $Zip }
|
if (Test-Path $Zip) { Remove-Item -Force $Zip }
|
||||||
# bsdtar writes forward-slash entry names (Compress-Archive on 5.1 doesn't).
|
# bsdtar writes forward-slash entry names (Compress-Archive on 5.1 doesn't).
|
||||||
|
|||||||
+2
-2
@@ -55,11 +55,11 @@ PROTO="$(sed -n 's/.*protoVersion = \([0-9]*\).*/\1/p' "$ROOT/packages/core/lib/
|
|||||||
if [[ -n "$LATEST" && "$LATEST" != "v$VER" ]]; then
|
if [[ -n "$LATEST" && "$LATEST" != "v$VER" ]]; then
|
||||||
NOTES="$(git -C "$ROOT" log -n 20 "$LATEST..HEAD" --format='- %s')"
|
NOTES="$(git -C "$ROOT" log -n 20 "$LATEST..HEAD" --format='- %s')"
|
||||||
else
|
else
|
||||||
NOTES="Watch together $VER."
|
NOTES="Starsync $VER."
|
||||||
fi
|
fi
|
||||||
export MANIFEST_FILE="$ROOT/updates/manifest.json" MANIFEST_VER="$VER" \
|
export MANIFEST_FILE="$ROOT/updates/manifest.json" MANIFEST_VER="$VER" \
|
||||||
MANIFEST_PROTO="$PROTO" MANIFEST_NOTES="$NOTES" \
|
MANIFEST_PROTO="$PROTO" MANIFEST_NOTES="$NOTES" \
|
||||||
MANIFEST_BASE="$SERVER/$REPO/releases/download/v$VER/watch-together-$VER"
|
MANIFEST_BASE="$SERVER/$REPO/releases/download/v$VER/starsync-$VER"
|
||||||
python3 - <<'EOF'
|
python3 - <<'EOF'
|
||||||
import json, os
|
import json, os
|
||||||
path = os.environ['MANIFEST_FILE']
|
path = os.environ['MANIFEST_FILE']
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Future<void> main(List<String> args) async {
|
|||||||
final prev = _git(root, ['describe', '--tags', '--abbrev=0', '$tag^'],
|
final prev = _git(root, ['describe', '--tags', '--abbrev=0', '$tag^'],
|
||||||
allowFail: true);
|
allowFail: true);
|
||||||
final notes = prev.isEmpty
|
final notes = prev.isEmpty
|
||||||
? 'Watch together $ver.'
|
? 'Starsync $ver.'
|
||||||
: _git(root, [
|
: _git(root, [
|
||||||
'log',
|
'log',
|
||||||
'$prev..$tag',
|
'$prev..$tag',
|
||||||
@@ -63,7 +63,7 @@ Future<void> main(List<String> args) async {
|
|||||||
rel = await c.post('$api/releases', {
|
rel = await c.post('$api/releases', {
|
||||||
'tag_name': tag,
|
'tag_name': tag,
|
||||||
'name': tag,
|
'name': tag,
|
||||||
'body': 'Watch together $ver\n\n$notes',
|
'body': 'Starsync $ver\n\n$notes',
|
||||||
'draft': false,
|
'draft': false,
|
||||||
'prerelease': false,
|
'prerelease': false,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user