From 51c1c5ab7887244516e6e8f10145d1f1a774e519 Mon Sep 17 00:00:00 2001 From: foxyz Date: Thu, 15 Oct 2026 20:21:20 +0300 Subject: [PATCH] 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--*. - 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 /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 --- .gitea/workflows/ci.yml | 4 +- .gitea/workflows/release.yml | 4 +- README.md | 42 +++-- apps/desktop/lib/main.dart | 7 +- apps/desktop/lib/src/platform/app_dirs.dart | 64 ------- apps/desktop/lib/src/platform/app_log.dart | 5 +- .../lib/src/platform/data_migration.dart | 116 ++++++++++++ .../platform/desktop/desktop_app_dirs.dart | 35 ++++ .../lib/src/platform/desktop_entry.dart | 34 +++- .../lib/src/platform/ports/app_dirs.dart | 69 ++++++++ .../desktop/lib/src/platform/ports/ports.dart | 1 + apps/desktop/lib/src/schedule/anilist.dart | 2 +- apps/desktop/lib/src/schedule/animeflow.dart | 2 +- apps/desktop/lib/src/state/settings.dart | 4 + .../lib/src/state/settings_schema.dart | 46 +++++ apps/desktop/lib/src/update/bundle/update.bat | 5 +- apps/desktop/lib/src/update/bundle/update.sh | 5 +- .../src/update/bundle/watch-together-server | 4 + .../lib/src/update/bundle/watch_together | 4 + .../lib/src/update/update_controller.dart | 4 +- apps/desktop/linux/CMakeLists.txt | 4 +- apps/desktop/pubspec.yaml | 4 +- apps/desktop/test/app_dirs_test.dart | 166 +++++++++++++++--- apps/desktop/test/app_keys_test.dart | 10 +- apps/desktop/test/avatar_cache_test.dart | 4 +- apps/desktop/test/chapters_mpv_e2e_test.dart | 6 +- apps/desktop/test/clock_offset_test.dart | 4 +- apps/desktop/test/controls_model_test.dart | 16 +- apps/desktop/test/coord_presence_test.dart | 8 +- apps/desktop/test/delivery_test.dart | 4 +- apps/desktop/test/desktop_entry_test.dart | 37 ++++ apps/desktop/test/end_of_file_e2e_test.dart | 2 +- apps/desktop/test/files_page_test.dart | 10 +- apps/desktop/test/files_scanner_test.dart | 4 +- apps/desktop/test/files_store_test.dart | 2 +- apps/desktop/test/files_thumbnails_test.dart | 2 +- apps/desktop/test/handoff_test.dart | 2 +- apps/desktop/test/home_test.dart | 20 +-- apps/desktop/test/jellyfin_accounts_test.dart | 8 +- apps/desktop/test/jellyfin_browser_test.dart | 12 +- apps/desktop/test/jellyfin_e2e_test.dart | 12 +- apps/desktop/test/jellyfin_session_test.dart | 22 +-- apps/desktop/test/library_rows_test.dart | 8 +- apps/desktop/test/library_tab_test.dart | 12 +- apps/desktop/test/media_marks_test.dart | 6 +- apps/desktop/test/minimal_controls_test.dart | 20 +-- apps/desktop/test/notice_table_test.dart | 4 +- apps/desktop/test/now_playing_info_test.dart | 4 +- apps/desktop/test/player_controls_test.dart | 10 +- apps/desktop/test/player_settings_test.dart | 24 +-- apps/desktop/test/player_status_test.dart | 20 +-- apps/desktop/test/queue_logic_test.dart | 2 +- apps/desktop/test/queue_panel_test.dart | 8 +- apps/desktop/test/queue_switch_e2e_test.dart | 16 +- apps/desktop/test/readahead_e2e_test.dart | 2 +- apps/desktop/test/relay_e2e_test.dart | 10 +- apps/desktop/test/release_name_test.dart | 2 +- apps/desktop/test/schedule_anilist_test.dart | 8 +- .../desktop/test/schedule_animeflow_test.dart | 6 +- apps/desktop/test/schedule_model_test.dart | 6 +- apps/desktop/test/schedule_page_test.dart | 14 +- apps/desktop/test/seek_drag_e2e_test.dart | 4 +- apps/desktop/test/server_isolate_test.dart | 10 +- apps/desktop/test/session_logic_test.dart | 4 +- apps/desktop/test/settings_schema_test.dart | 57 ++++++ apps/desktop/test/sfx_test.dart | 6 +- apps/desktop/test/shaders_mpv_test.dart | 8 +- apps/desktop/test/shaders_test.dart | 6 +- apps/desktop/test/shell_test.dart | 24 +-- apps/desktop/test/sound_device_e2e_test.dart | 8 +- apps/desktop/test/sound_engine_test.dart | 10 +- apps/desktop/test/state_diff_test.dart | 2 +- apps/desktop/test/sync_playback_e2e_test.dart | 2 +- apps/desktop/test/text_focus_test.dart | 2 +- apps/desktop/test/update_controller_test.dart | 96 +++++----- apps/desktop/test/user_seek_test.dart | 2 +- apps/desktop/test/video_reload_e2e_test.dart | 4 +- apps/desktop/test/volume_readout_test.dart | 10 +- apps/desktop/test/ytdl_headers_e2e_test.dart | 2 +- apps/desktop/windows/CMakeLists.txt | 4 +- apps/desktop/windows/runner/Runner.rc | 12 +- apps/desktop/windows/runner/win32_window.cpp | 2 +- ...atch-together.service => starsync.service} | 17 +- docs/ARCHITECTURE.md | 90 ++++++++-- docs/DEVELOPMENT.md | 16 +- docs/HOW-IT-WORKS.md | 2 +- docs/ROADMAP.md | 28 +-- packages/core/lib/src/diag_share.dart | 2 +- packages/core/lib/src/updater.dart | 53 +++++- packages/core/test/updater_test.dart | 105 +++++++++-- packages/server/bin/server.dart | 9 +- tool/dev_release.sh | 4 +- tool/package_linux.sh | 27 +-- tool/package_windows.ps1 | 24 +-- tool/release.sh | 4 +- tool/release_publish.dart | 4 +- 96 files changed, 1169 insertions(+), 489 deletions(-) delete mode 100644 apps/desktop/lib/src/platform/app_dirs.dart create mode 100644 apps/desktop/lib/src/platform/data_migration.dart create mode 100644 apps/desktop/lib/src/platform/desktop/desktop_app_dirs.dart create mode 100644 apps/desktop/lib/src/platform/ports/app_dirs.dart create mode 100644 apps/desktop/lib/src/state/settings_schema.dart create mode 100755 apps/desktop/lib/src/update/bundle/watch-together-server create mode 100755 apps/desktop/lib/src/update/bundle/watch_together create mode 100644 apps/desktop/test/desktop_entry_test.dart create mode 100644 apps/desktop/test/settings_schema_test.dart rename deploy/{watch-together.service => starsync.service} (52%) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 18a025f..546b027 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -35,9 +35,9 @@ jobs: - run: dart test working-directory: packages/server # 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 - - run: build/watch-together-server --help + - run: build/starsync-server --help working-directory: packages/server # The updater's Windows apply path (running exe parked as .old) only diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index c5b0dbc..2a93f07 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -38,7 +38,7 @@ jobs: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | 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: # yuuka-windows: host-mode runner (no containers), runs as SYSTEM, has @@ -73,5 +73,5 @@ jobs: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | $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 } diff --git a/README.md b/README.md index cd9c51c..2b3e044 100644 --- a/README.md +++ b/README.md @@ -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. This is a desktop app that drives mpv. -Name: `watch-together` is a placeholder and the final name is undecided -(`sync-relay` is the working name). It appears in file names and the -update manifest until that is settled. +Up to 1.2.x the app was called `watch_together` (its files +`watch-together-*`); the repository and the update manifest keep their old +addresses for now. - Desktop only (Linux and Windows), always mpv. No web clients. - 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: -- Linux: `watch-together--linux-x64.tar.gz`. Extract, run - `watch_together`. On start it adds itself to your app launcher (and its +- Linux: `starsync--linux-x64.tar.gz`. Extract, run + `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`). -- Windows: `watch-together--windows-x64.zip`. Extract, run - `watch_together.exe`. mpv is bundled. New and not yet tried on a real +- Windows: `starsync--windows-x64.zip`. Extract, run + `starsync.exe`. mpv is bundled. New and not yet tried on a real 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 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, 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 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 -server box too), `watch_together --upgrade`, or the shipped `update.sh` / +`./starsync-server --upgrade` (no display needed, so it works on a +server box too), `./starsync --upgrade`, or the shipped `update.sh` / `update.bat` (which run the first). Each updates the whole folder, app and server; restart whichever is running to use the new version. A 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 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. ``` -./watch-together-server --port 8080 --secret +./starsync-server --port 8080 --secret ``` | Flag | Default | Meaning | @@ -245,9 +257,11 @@ then everyone (you included) uses Join. It prints `READY port=... name=...` once listening and stops cleanly on 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 -[deploy/watch-together.service](deploy/watch-together.service) (copy it to -`/etc/systemd/system/`, edit the flags, `systemctl enable --now watch-together`). +To run it as a service, unpack the tarball to `/opt/starsync` and use +[deploy/starsync.service](deploy/starsync.service) (copy it to +`/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 app's recent log (passwords, tokens and cookies left out) on the diff --git a/apps/desktop/lib/main.dart b/apps/desktop/lib/main.dart index d86990b..a1f97fa 100644 --- a/apps/desktop/lib/main.dart +++ b/apps/desktop/lib/main.dart @@ -7,9 +7,9 @@ import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'app.dart'; -import 'src/platform/app_dirs.dart'; import 'src/platform/app_log.dart'; import 'src/platform/app_shutdown.dart'; +import 'src/platform/desktop/desktop_app_dirs.dart'; import 'src/platform/desktop_entry.dart'; import 'src/screens/home/ui.dart'; import 'src/session/app_events.dart' show UpdateAvailable; @@ -42,10 +42,9 @@ Future main(List args) async { ), ); WidgetsFlutterBinding.ensureInitialized(); - final dirs = await AppDirs.resolve(); + final dirs = await const DesktopAppDirs().resolve(); await startAppLogging(dirs); - // An absolute subDir replaces the documents folder (package:path join). - await Hive.initFlutter(dirs.hive); + Hive.init(dirs.hive); await AppSettings.instance.ensureLoaded(); // Close button / Alt+F4: the orderly close ([shutDownApp]), so others // see "left", a hosted server shuts down and no player outlives the diff --git a/apps/desktop/lib/src/platform/app_dirs.dart b/apps/desktop/lib/src/platform/app_dirs.dart deleted file mode 100644 index be027c3..0000000 --- a/apps/desktop/lib/src/platform/app_dirs.dart +++ /dev/null @@ -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 -/// `/logs`. With `STARSYNC_PROFILE=` (letters, digits, -/// `-`, `_`; at most 32) the per-install data moves to -/// `/profiles//`: 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 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 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 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; - } -} diff --git a/apps/desktop/lib/src/platform/app_log.dart b/apps/desktop/lib/src/platform/app_log.dart index 43c4ebd..6ff370b 100644 --- a/apps/desktop/lib/src/platform/app_log.dart +++ b/apps/desktop/lib/src/platform/app_log.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:sync_relay_core/core.dart'; import '../screens/home/ui.dart' show appName, displayVersion; -import 'app_dirs.dart'; +import 'ports/app_dirs.dart'; import '../state/session_state.dart'; // The app's log on disk (/logs/app-YYYY-MM-DD.log; core's @@ -39,6 +39,9 @@ Future startAppLogging(AppDirs dirs) async { _appLog.info('$appName ${displayVersion()} on ${Platform.operatingSystem} ' '${Platform.operatingSystemVersion}, pid $pid, Dart ${Platform.version.split(' ').first}' '${dirs.profile == null ? '' : ', profile ${dirs.profile}'}'); + for (final line in dirs.migrated) { + _appLog.info(line); + } final bad = AppDirs.invalidProfile(Platform.environment); if (bad != null) _appLog.warning('$profileEnv="$bad" ignored: use letters, digits, - and _ (at most 32)'); } diff --git a/apps/desktop/lib/src/platform/data_migration.dart b/apps/desktop/lib/src/platform/data_migration.dart new file mode 100644 index 0000000..4686975 --- /dev/null +++ b/apps/desktop/lib/src/platform/data_migration.dart @@ -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 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> migrateLegacyData( + {required String support, required String hive, required List oldSupport, String? documents}) async { + final sep = Platform.pathSeparator; + final marker = File('$support$sep$migrationMarker'); + if (marker.existsSync()) return const []; + final lines = []; + 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 = []; + 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 _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; +} diff --git a/apps/desktop/lib/src/platform/desktop/desktop_app_dirs.dart b/apps/desktop/lib/src/platform/desktop/desktop_app_dirs.dart new file mode 100644 index 0000000..f3f6845 --- /dev/null +++ b/apps/desktop/lib/src/platform/desktop/desktop_app_dirs.dart @@ -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 resolve() async { + final support = (await getApplicationSupportDirectory()).path; + final base = AppDirs.fromEnv(support: support, env: const {}); + List 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; + } +} diff --git a/apps/desktop/lib/src/platform/desktop_entry.dart b/apps/desktop/lib/src/platform/desktop_entry.dart index e1a3365..e65678a 100644 --- a/apps/desktop/lib/src/platform/desktop_entry.dart +++ b/apps/desktop/lib/src/platform/desktop_entry.dart @@ -5,16 +5,24 @@ import 'package:sync_relay_core/core.dart' show findInstallRoot; /// The app's id: GTK's application id, so the Wayland `app_id` the /// compositor matches to [desktopFileName] for the window's icon. -const appId = 'moe.senkochan.watch_together'; +const appId = 'dev.foxyz.starsync'; const desktopFileName = '$appId.desktop'; +/// The app id up to 1.2.x: its entry and icons are removed (only those +/// exact files, which that version wrote). +const legacyAppId = 'moe.senkochan.watch_together'; + +const _iconSizes = [16, 32, 48, 64, 128, 256, 512]; + /// Linux release installs (an unpacked tarball) carry no launcher entry, so /// Wayland shows a generic icon. On start: write the `.desktop` file into /// `~/.local/share/applications` (Exec = this binary) and the icon into /// the hicolor theme, only when something changed (a moved install, a new -/// icon). Never in debug runs; failures only log. -Future installDesktopEntry({String? executable, String? dataHome, String? home}) async { - if (!Platform.isLinux || kDebugMode) return; +/// icon). The entry and icons of [legacyAppId] go. Never in debug runs +/// (unless [force], for tests); failures only log. +Future installDesktopEntry( + {String? executable, String? dataHome, String? home, @visibleForTesting bool force = false}) async { + if (!Platform.isLinux || (kDebugMode && !force)) return; try { final exe = executable ?? Platform.resolvedExecutable; final root = findInstallRoot(exe, windows: false); @@ -22,7 +30,8 @@ Future installDesktopEntry({String? executable, String? dataHome, String? final share = dataHome ?? Platform.environment['XDG_DATA_HOME'] ?? '${home ?? Platform.environment['HOME']}/.local/share'; final icons = '${root.path}/data/flutter_assets/assets/icon'; - for (final size in const [16, 32, 48, 64, 128, 256, 512]) { + await _removeLegacy(share); + for (final size in _iconSizes) { final src = File('$icons/icon_$size.png'); if (!src.existsSync()) continue; final dst = File('$share/icons/hicolor/${size}x$size/apps/$appId.png'); @@ -40,6 +49,21 @@ Future installDesktopEntry({String? executable, String? dataHome, String? } } +Future _removeLegacy(String share) async { + for (final f in [ + File('$share/applications/$legacyAppId.desktop'), + for (final size in _iconSizes) File('$share/icons/hicolor/${size}x$size/apps/$legacyAppId.png'), + ]) { + if (!f.existsSync()) continue; + try { + await f.delete(); + debugPrint('desktop entry: removed ${f.path}'); + } catch (e) { + debugPrint('desktop entry: cannot remove ${f.path}: $e'); + } + } +} + @visibleForTesting String desktopEntryText(String exe) => '''[Desktop Entry] Type=Application diff --git a/apps/desktop/lib/src/platform/ports/app_dirs.dart b/apps/desktop/lib/src/platform/ports/app_dirs.dart new file mode 100644 index 0000000..1167b6a --- /dev/null +++ b/apps/desktop/lib/src/platform/ports/app_dirs.dart @@ -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 +/// `/hive`, the logs in `/logs`. With +/// `STARSYNC_PROFILE=` (letters, digits, `-`, `_`; at most 32) the +/// per-install data moves to `/profiles//`: 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 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 env, List 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 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 resolve(); +} diff --git a/apps/desktop/lib/src/platform/ports/ports.dart b/apps/desktop/lib/src/platform/ports/ports.dart index 7e9a19a..bb55dd6 100644 --- a/apps/desktop/lib/src/platform/ports/ports.dart +++ b/apps/desktop/lib/src/platform/ports/ports.dart @@ -4,6 +4,7 @@ import 'shell_port.dart'; import 'system_notifications.dart'; import 'url_opener.dart'; +export 'app_dirs.dart'; export 'folder_access.dart'; export 'link_resolver.dart'; export 'shell_port.dart'; diff --git a/apps/desktop/lib/src/schedule/anilist.dart b/apps/desktop/lib/src/schedule/anilist.dart index 112e0ae..e6fd6ac 100644 --- a/apps/desktop/lib/src/schedule/anilist.dart +++ b/apps/desktop/lib/src/schedule/anilist.dart @@ -158,7 +158,7 @@ class AniListClient { _cacheDir = cacheDir ?? _defaultCacheDir, _delay = delay ?? Future.delayed, _now = now ?? DateTime.now, - userAgent = userAgent ?? 'watch-together/$appVersion'; + userAgent = userAgent ?? 'Starsync/$appVersion'; final Uri url; final Duration memoryTtl; diff --git a/apps/desktop/lib/src/schedule/animeflow.dart b/apps/desktop/lib/src/schedule/animeflow.dart index fbd102d..8ac8024 100644 --- a/apps/desktop/lib/src/schedule/animeflow.dart +++ b/apps/desktop/lib/src/schedule/animeflow.dart @@ -474,7 +474,7 @@ class AnimeFlowClient { _ownsHttp = http == null, _delay = delay ?? Future.delayed, _now = now ?? DateTime.now, - userAgent = userAgent ?? 'watch-together/$appVersion'; + userAgent = userAgent ?? 'Starsync/$appVersion'; /// Authenticated with the linked Jellyfin account on the same domain /// (see [tokenFor]), looked up on every call so a re-link applies. diff --git a/apps/desktop/lib/src/state/settings.dart b/apps/desktop/lib/src/state/settings.dart index 1c83095..f9113e0 100644 --- a/apps/desktop/lib/src/state/settings.dart +++ b/apps/desktop/lib/src/state/settings.dart @@ -6,6 +6,7 @@ import 'package:signals/signals_flutter.dart'; import '../controls/model/controls_prefs.dart'; import '../shaders/shader_presets.dart'; +import 'settings_schema.dart'; // App preferences (Settings page), one Hive box. Signals start at their // defaults and take the stored values once [AppSettings.ensureLoaded] @@ -134,6 +135,9 @@ class AppSettings { Future _loadNow() async { try { final box = _box = await Hive.openBox(settingsBoxName); + for (final line in await migrateSettings(box)) { + debugPrint(line); + } final mode = box.get(_keyHwdecMode); hwdecMode.value = HwdecMode.values .where((m) => m.name == mode) diff --git a/apps/desktop/lib/src/state/settings_schema.dart b/apps/desktop/lib/src/state/settings_schema.dart new file mode 100644 index 0000000..6a3acdf --- /dev/null +++ b/apps/desktop/lib/src/state/settings_schema.dart @@ -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 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 = []; + +/// 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> migrateSettings(Box box, + {List 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 = []; + 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; +} diff --git a/apps/desktop/lib/src/update/bundle/update.bat b/apps/desktop/lib/src/update/bundle/update.bat index 2430180..055d9a9 100644 --- a/apps/desktop/lib/src/update/bundle/update.bat +++ b/apps/desktop/lib/src/update/bundle/update.bat @@ -1,4 +1,7 @@ @echo off rem Update this install in place (same as clicking Update in the app). -"%~dp0watch-together-server.exe" --upgrade %* +rem The old server name is the fallback for installs from before the rename. +set "SERVER=%~dp0starsync-server.exe" +if not exist "%SERVER%" set "SERVER=%~dp0watch-together-server.exe" +"%SERVER%" --upgrade %* pause diff --git a/apps/desktop/lib/src/update/bundle/update.sh b/apps/desktop/lib/src/update/bundle/update.sh index eddaeb0..ef509fd 100755 --- a/apps/desktop/lib/src/update/bundle/update.sh +++ b/apps/desktop/lib/src/update/bundle/update.sh @@ -1,4 +1,7 @@ #!/bin/sh # Update this install in place (same as clicking Update in the app). # The server binary runs the updater: it needs no GTK/display (VPS). -cd "$(dirname "$0")" && exec ./watch-together-server --upgrade "$@" +# The old server name is the fallback for installs from before the rename. +cd "$(dirname "$0")" || exit 1 +if [ -x ./starsync-server ]; then exec ./starsync-server --upgrade "$@"; fi +exec ./watch-together-server --upgrade "$@" diff --git a/apps/desktop/lib/src/update/bundle/watch-together-server b/apps/desktop/lib/src/update/bundle/watch-together-server new file mode 100755 index 0000000..5f0f209 --- /dev/null +++ b/apps/desktop/lib/src/update/bundle/watch-together-server @@ -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" "$@" diff --git a/apps/desktop/lib/src/update/bundle/watch_together b/apps/desktop/lib/src/update/bundle/watch_together new file mode 100755 index 0000000..54e3de4 --- /dev/null +++ b/apps/desktop/lib/src/update/bundle/watch_together @@ -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" "$@" diff --git a/apps/desktop/lib/src/update/update_controller.dart b/apps/desktop/lib/src/update/update_controller.dart index 3d951e3..fa99bdb 100644 --- a/apps/desktop/lib/src/update/update_controller.dart +++ b/apps/desktop/lib/src/update/update_controller.dart @@ -173,6 +173,8 @@ class UpdateController { _log.info('updated to ${m.version}, restarting'); status.value = UpdateStatus(UpdatePhase.restarting, manifest: m, canInstall: true); - await restart(_exe, launchArgs); + // Started by an old name (1.2.x's restart, an old launcher): the new + // binary next to it. + await restart(relaunchExecutable(_exe), launchArgs); } } diff --git a/apps/desktop/linux/CMakeLists.txt b/apps/desktop/linux/CMakeLists.txt index 54f226b..1e51efa 100644 --- a/apps/desktop/linux/CMakeLists.txt +++ b/apps/desktop/linux/CMakeLists.txt @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "watch_together") +set(BINARY_NAME "starsync") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "moe.senkochan.watch_together") +set(APPLICATION_ID "dev.foxyz.starsync") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/apps/desktop/pubspec.yaml b/apps/desktop/pubspec.yaml index 525f9a0..4772565 100644 --- a/apps/desktop/pubspec.yaml +++ b/apps/desktop/pubspec.yaml @@ -1,5 +1,5 @@ -name: watch_together -description: Watch-together desktop app (working title, repo sync_relay_project). One binary. +name: starsync +description: Starsync desktop app (repo sync_relay_project). One binary. publish_to: none version: 1.2.0 environment: diff --git a/apps/desktop/test/app_dirs_test.dart b/apps/desktop/test/app_dirs_test.dart index e965846..cefd384 100644 --- a/apps/desktop/test/app_dirs_test.dart +++ b/apps/desktop/test/app_dirs_test.dart @@ -1,36 +1,158 @@ -// restructure: rewrite in step R (Rename): Hive moves into the support -// folder for the normal app too. +import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/platform/app_dirs.dart'; +import 'package:starsync/src/files/folder_store.dart' show filesBoxName; +import 'package:starsync/src/jellyfin/accounts.dart' show jellyfinBoxName; +import 'package:starsync/src/jellyfin/track_prefs.dart' show trackPrefsBoxName; +import 'package:starsync/src/platform/data_migration.dart'; +import 'package:starsync/src/platform/ports/app_dirs.dart'; +import 'package:starsync/src/state/connect_prefs.dart' show connectFormBoxName; +import 'package:starsync/src/state/settings.dart' show settingsBoxName; void main() { final sep = Platform.pathSeparator; const support = '/s'; - test('no profile: today\'s layout', () { - final d = AppDirs.fromEnv(support: support, env: const {}); - expect(d.profile, isNull); - expect(d.hive, isNull, reason: "Hive's default folder"); - expect(d.logs, '$support${sep}logs'); + group('AppDirs', () { + test('no profile: Hive and logs in the support folder', () { + final d = AppDirs.fromEnv(support: support, env: const {}); + expect(d.profile, isNull); + expect(d.hive, '$support${sep}hive'); + expect(d.logs, '$support${sep}logs'); + }); + + test('a profile gets its own Hive and logs folders', () { + final d = AppDirs.fromEnv(support: support, env: const {profileEnv: 'b'}); + expect(d.profile, 'b'); + expect(d.hive, '$support${sep}profiles${sep}b${sep}hive'); + expect(d.logs, '$support${sep}profiles${sep}b${sep}logs'); + expect(d.support, support, reason: 'yt-dlp and shaders stay shared'); + }); + + test('an unusable name is ignored and reported', () { + for (final bad in ['../x', 'a b', 'x' * 33, 'é']) { + final env = {profileEnv: bad}; + expect(AppDirs.fromEnv(support: support, env: env).profile, isNull, reason: bad); + expect(AppDirs.invalidProfile(env), bad); + } + expect(AppDirs.invalidProfile(const {profileEnv: ''}), isNull, reason: 'empty is just unset'); + expect(AppDirs.invalidProfile(const {profileEnv: 'ok_1-2'}), isNull); + }); }); - test('a profile gets its own Hive and logs folders', () { - final d = AppDirs.fromEnv(support: support, env: const {profileEnv: 'b'}); - expect(d.profile, 'b'); - expect(d.hive, '$support${sep}profiles${sep}b${sep}hive'); - expect(d.logs, '$support${sep}profiles${sep}b${sep}logs'); - expect(d.support, support, reason: 'yt-dlp and shaders stay shared'); + test('the migrated boxes are the ones the app opens', () { + expect(appHiveBoxes.toSet(), + {settingsBoxName, connectFormBoxName, filesBoxName, jellyfinBoxName, trackPrefsBoxName}); }); - test('an unusable name is ignored and reported', () { - for (final bad in ['../x', 'a b', 'x' * 33, 'é']) { - final env = {profileEnv: bad}; - expect(AppDirs.fromEnv(support: support, env: env).profile, isNull, reason: bad); - expect(AppDirs.invalidProfile(env), bad); - } - expect(AppDirs.invalidProfile(const {profileEnv: ''}), isNull, reason: 'empty is just unset'); - expect(AppDirs.invalidProfile(const {profileEnv: 'ok_1-2'}), isNull); + test('old support folders next to the new one', () { + expect(legacySupportDirs('/home/u/.local/share/dev.foxyz.starsync', windows: false), [ + '/home/u/.local/share/moe.senkochan.watch_together', + '/home/u/.local/share/watch_together', + ]); + expect(legacySupportDirs(r'C:\Users\u\AppData\Roaming\foxyz\Starsync', windows: true), + [r'C:\Users\u\AppData\Roaming\com.example\watch_together']); + }); + + group('migration from before the rename', () { + late Directory home; + late String share, oldSupport, support, docs, hive; + + setUp(() async { + home = await Directory.systemTemp.createTemp('starsync-mig'); + share = '${home.path}/.local/share'; + oldSupport = '$share/moe.senkochan.watch_together'; + support = '$share/dev.foxyz.starsync'; + docs = '${home.path}/Documents'; + hive = '$support/hive'; + // The old support folder as 1.2.x left it. + await Directory('$oldSupport/yt-dlp').create(recursive: true); + await File('$oldSupport/yt-dlp/cookies.txt').writeAsString('# Netscape cookies'); + await File('$oldSupport/yt-dlp/yt-dlp').writeAsString('bin'); + await Directory('$oldSupport/logs').create(); + await File('$oldSupport/logs/app-2026-01-01.log').writeAsString('log'); + await Directory('$oldSupport/shaders').create(); + await File('$oldSupport/shaders/a.glsl').writeAsString('shader'); + await Directory('$oldSupport/profiles/b/hive').create(recursive: true); + await File('$oldSupport/profiles/b/hive/settings.hive').writeAsString('b-settings'); + if (!Platform.isWindows) { + await Process.run('chmod', ['600', '$oldSupport/yt-dlp/cookies.txt']); + await Process.run('chmod', ['755', '$oldSupport/yt-dlp/yt-dlp']); + await Link('$oldSupport/yt-dlp/current').create('yt-dlp'); + } + // The documents folder: this app's boxes and another app's. + await Directory(docs).create(recursive: true); + for (final n in ['settings', 'jellyfin', 'files', 'track_prefs', 'connect_form', 'app', 'credentials']) { + await File('$docs/$n.hive').writeAsString('$n-data'); + await File('$docs/$n.lock').writeAsString(''); + } + // path_provider creates the new folder before the app looks. + await Directory(support).create(recursive: true); + }); + + tearDown(() => home.delete(recursive: true)); + + Future> 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); + }); }); } diff --git a/apps/desktop/test/app_keys_test.dart b/apps/desktop/test/app_keys_test.dart index 4c3f660..78d9a8b 100644 --- a/apps/desktop/test/app_keys_test.dart +++ b/apps/desktop/test/app_keys_test.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/shell/app_shell.dart'; -import 'package:watch_together/src/state/session_state.dart'; -import 'package:watch_together/src/update/update_controller.dart'; -import 'package:watch_together/src/widgets/app_keys.dart'; -import 'package:watch_together/src/widgets/text_focus.dart'; +import 'package:starsync/src/shell/app_shell.dart'; +import 'package:starsync/src/state/session_state.dart'; +import 'package:starsync/src/update/update_controller.dart'; +import 'package:starsync/src/widgets/app_keys.dart'; +import 'package:starsync/src/widgets/text_focus.dart'; void main() { final session = SessionState.instance; diff --git a/apps/desktop/test/avatar_cache_test.dart b/apps/desktop/test/avatar_cache_test.dart index 885cf05..a2d02ff 100644 --- a/apps/desktop/test/avatar_cache_test.dart +++ b/apps/desktop/test/avatar_cache_test.dart @@ -5,8 +5,8 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/sync/avatar_cache.dart'; -import 'package:watch_together/src/sync/sync_client.dart'; +import 'package:starsync/src/sync/avatar_cache.dart'; +import 'package:starsync/src/sync/sync_client.dart'; Future _poll(FutureOr Function() cond, String what) async { final deadline = DateTime.now().add(const Duration(seconds: 10)); diff --git a/apps/desktop/test/chapters_mpv_e2e_test.dart b/apps/desktop/test/chapters_mpv_e2e_test.dart index aa4b551..6516b85 100644 --- a/apps/desktop/test/chapters_mpv_e2e_test.dart +++ b/apps/desktop/test/chapters_mpv_e2e_test.dart @@ -5,9 +5,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/playback/media_marks.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; -import 'package:watch_together/src/sync/state_diff.dart'; +import 'package:starsync/src/playback/media_marks.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; +import 'package:starsync/src/sync/state_diff.dart'; final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path; diff --git a/apps/desktop/test/clock_offset_test.dart b/apps/desktop/test/clock_offset_test.dart index b01819e..425c9df 100644 --- a/apps/desktop/test/clock_offset_test.dart +++ b/apps/desktop/test/clock_offset_test.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/sync/state_diff.dart'; -import 'package:watch_together/src/sync/sync_client.dart'; +import 'package:starsync/src/sync/state_diff.dart'; +import 'package:starsync/src/sync/sync_client.dart'; Future _poll(FutureOr Function() cond, String what) async { final end = DateTime.now().add(const Duration(seconds: 10)); diff --git a/apps/desktop/test/controls_model_test.dart b/apps/desktop/test/controls_model_test.dart index 98a4cee..0a17630 100644 --- a/apps/desktop/test/controls_model_test.dart +++ b/apps/desktop/test/controls_model_test.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:signals/signals_flutter.dart'; -import 'package:watch_together/src/controls/controls_probe.dart'; -import 'package:watch_together/src/controls/model/controls_layout.dart'; -import 'package:watch_together/src/controls/model/controls_model.dart'; -import 'package:watch_together/src/controls/model/controls_prefs.dart'; -import 'package:watch_together/src/controls/session_controls.dart'; -import 'package:watch_together/src/playback/media_marks.dart'; -import 'package:watch_together/src/state/session_state.dart'; -import 'package:watch_together/src/state/settings.dart'; +import 'package:starsync/src/controls/controls_probe.dart'; +import 'package:starsync/src/controls/model/controls_layout.dart'; +import 'package:starsync/src/controls/model/controls_model.dart'; +import 'package:starsync/src/controls/model/controls_prefs.dart'; +import 'package:starsync/src/controls/session_controls.dart'; +import 'package:starsync/src/playback/media_marks.dart'; +import 'package:starsync/src/state/session_state.dart'; +import 'package:starsync/src/state/settings.dart'; // The controls model and its layout spec: which controls the bar has, in // which order, what goes to ⚙; and both skins drawing the same order. diff --git a/apps/desktop/test/coord_presence_test.dart b/apps/desktop/test/coord_presence_test.dart index 72e0414..98e3aab 100644 --- a/apps/desktop/test/coord_presence_test.dart +++ b/apps/desktop/test/coord_presence_test.dart @@ -8,10 +8,10 @@ import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; import 'package:hive_ce/hive.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; -import 'package:watch_together/src/sfx/sfx.dart'; -import 'package:watch_together/src/state/session_state.dart'; -import 'package:watch_together/src/sync/sync_client.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; +import 'package:starsync/src/sfx/sfx.dart'; +import 'package:starsync/src/state/session_state.dart'; +import 'package:starsync/src/sync/sync_client.dart'; Future _poll( FutureOr Function() cond, diff --git a/apps/desktop/test/delivery_test.dart b/apps/desktop/test/delivery_test.dart index db6e6f1..53abec0 100644 --- a/apps/desktop/test/delivery_test.dart +++ b/apps/desktop/test/delivery_test.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/session/delivery.dart'; -import 'package:watch_together/src/shell/delivery_line.dart'; +import 'package:starsync/src/session/delivery.dart'; +import 'package:starsync/src/shell/delivery_line.dart'; MemberView _m(String id, String name, {double buffered = 30, bool host = false, int rate = 0}) => MemberView(id: id, name: name, ready: true, inSet: true, buffered: buffered, stale: false, host: host, rate: rate); diff --git a/apps/desktop/test/desktop_entry_test.dart b/apps/desktop/test/desktop_entry_test.dart new file mode 100644 index 0000000..5fdc193 --- /dev/null +++ b/apps/desktop/test/desktop_entry_test.dart @@ -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'); +} diff --git a/apps/desktop/test/end_of_file_e2e_test.dart b/apps/desktop/test/end_of_file_e2e_test.dart index 34e3413..ee0a820 100644 --- a/apps/desktop/test/end_of_file_e2e_test.dart +++ b/apps/desktop/test/end_of_file_e2e_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; const _mediaPath = '/home/foxyz/Videos/chiyo.mp4'; // 13.8 s diff --git a/apps/desktop/test/files_page_test.dart b/apps/desktop/test/files_page_test.dart index dd5d951..7c47d05 100644 --- a/apps/desktop/test/files_page_test.dart +++ b/apps/desktop/test/files_page_test.dart @@ -7,11 +7,11 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive_ce/hive.dart'; -import 'package:watch_together/src/files/folder_store.dart'; -import 'package:watch_together/src/files/scanner.dart'; -import 'package:watch_together/src/files/thumbnails.dart'; -import 'package:watch_together/src/files/ui/file_tiles.dart'; -import 'package:watch_together/src/files/ui/files_page.dart'; +import 'package:starsync/src/files/folder_store.dart'; +import 'package:starsync/src/files/scanner.dart'; +import 'package:starsync/src/files/thumbnails.dart'; +import 'package:starsync/src/files/ui/file_tiles.dart'; +import 'package:starsync/src/files/ui/files_page.dart'; // The Files page over a real (tiny) folder tree: the scan runs in-line // instead of in an isolate and thumbnails come from a fake loader, so diff --git a/apps/desktop/test/files_scanner_test.dart b/apps/desktop/test/files_scanner_test.dart index 7713e64..7cb52cc 100644 --- a/apps/desktop/test/files_scanner_test.dart +++ b/apps/desktop/test/files_scanner_test.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/files/name_parser.dart'; -import 'package:watch_together/src/files/scanner.dart'; +import 'package:starsync/src/files/name_parser.dart'; +import 'package:starsync/src/files/scanner.dart'; late Directory _root; diff --git a/apps/desktop/test/files_store_test.dart b/apps/desktop/test/files_store_test.dart index 1200206..e57d692 100644 --- a/apps/desktop/test/files_store_test.dart +++ b/apps/desktop/test/files_store_test.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive_ce/hive.dart'; -import 'package:watch_together/src/files/folder_store.dart'; +import 'package:starsync/src/files/folder_store.dart'; void main() { test('folders and recent files persist across a reload', () async { diff --git a/apps/desktop/test/files_thumbnails_test.dart b/apps/desktop/test/files_thumbnails_test.dart index b9f097d..1d1cde1 100644 --- a/apps/desktop/test/files_thumbnails_test.dart +++ b/apps/desktop/test/files_thumbnails_test.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:media_kit/media_kit.dart'; -import 'package:watch_together/src/files/thumbnails.dart'; +import 'package:starsync/src/files/thumbnails.dart'; // Real libmpv: frames grabbed with vo=null + screenshot-to-file. The test // media comes from ffmpeg (this machine only; the app never needs it). diff --git a/apps/desktop/test/handoff_test.dart b/apps/desktop/test/handoff_test.dart index b6ae49f..7efa8d2 100644 --- a/apps/desktop/test/handoff_test.dart +++ b/apps/desktop/test/handoff_test.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/shell/handoff.dart'; +import 'package:starsync/src/shell/handoff.dart'; // What a drop or a paste is: decided from paths and text alone (the file // system stands behind a fake). diff --git a/apps/desktop/test/home_test.dart b/apps/desktop/test/home_test.dart index ac79110..3663956 100644 --- a/apps/desktop/test/home_test.dart +++ b/apps/desktop/test/home_test.dart @@ -5,17 +5,17 @@ import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/files/ui/files_page.dart'; -import 'package:watch_together/src/schedule/ui/schedule_page.dart'; +import 'package:starsync/src/files/ui/files_page.dart'; +import 'package:starsync/src/schedule/ui/schedule_page.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/screens/home/health_probe.dart'; -import 'package:watch_together/src/screens/home/media_picker.dart'; -import 'package:watch_together/src/screens/home/update_indicator.dart'; -import 'package:watch_together/src/shell/app_shell.dart'; -import 'package:watch_together/src/shell/library_link.dart' show soloPlayHint; -import 'package:watch_together/src/state/session_state.dart'; -import 'package:watch_together/src/state/settings.dart'; -import 'package:watch_together/src/update/update_controller.dart'; +import 'package:starsync/src/screens/home/health_probe.dart'; +import 'package:starsync/src/screens/home/media_picker.dart'; +import 'package:starsync/src/screens/home/update_indicator.dart'; +import 'package:starsync/src/shell/app_shell.dart'; +import 'package:starsync/src/shell/library_link.dart' show soloPlayHint; +import 'package:starsync/src/state/session_state.dart'; +import 'package:starsync/src/state/settings.dart'; +import 'package:starsync/src/update/update_controller.dart'; RecentServer join(String host, String port, [String secret = '']) => RecentServer(hosted: false, host: host, port: port, secret: secret); diff --git a/apps/desktop/test/jellyfin_accounts_test.dart b/apps/desktop/test/jellyfin_accounts_test.dart index 7118e93..a77826a 100644 --- a/apps/desktop/test/jellyfin_accounts_test.dart +++ b/apps/desktop/test/jellyfin_accounts_test.dart @@ -4,10 +4,10 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive_ce/hive.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/jellyfin/format.dart'; -import 'package:watch_together/src/jellyfin/sign_in.dart'; -import 'package:watch_together/src/screens/home/media_picker.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/jellyfin/format.dart'; +import 'package:starsync/src/jellyfin/sign_in.dart'; +import 'package:starsync/src/screens/home/media_picker.dart'; /// Captured Jellyfin 10.11 answers shared with packages/core's client tests. String jellyfinFixture(String name) => diff --git a/apps/desktop/test/jellyfin_browser_test.dart b/apps/desktop/test/jellyfin_browser_test.dart index 67768b2..5584adf 100644 --- a/apps/desktop/test/jellyfin_browser_test.dart +++ b/apps/desktop/test/jellyfin_browser_test.dart @@ -5,12 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/jellyfin/accounts_panel.dart'; -import 'package:watch_together/src/jellyfin/browser.dart'; -import 'package:watch_together/src/jellyfin/browse_scope.dart'; -import 'package:watch_together/src/jellyfin/picker_source.dart'; -import 'package:watch_together/src/state/settings.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/jellyfin/accounts_panel.dart'; +import 'package:starsync/src/jellyfin/browser.dart'; +import 'package:starsync/src/jellyfin/browse_scope.dart'; +import 'package:starsync/src/jellyfin/picker_source.dart'; +import 'package:starsync/src/state/settings.dart'; import 'jellyfin_accounts_test.dart' show FakeJellyfin, arifuretaId, fixtureServerId, linkedAccounts, season2Id; diff --git a/apps/desktop/test/jellyfin_e2e_test.dart b/apps/desktop/test/jellyfin_e2e_test.dart index 0748bb8..b5bcea8 100644 --- a/apps/desktop/test/jellyfin_e2e_test.dart +++ b/apps/desktop/test/jellyfin_e2e_test.dart @@ -8,12 +8,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/jellyfin/session_media.dart'; -import 'package:watch_together/src/jellyfin/track_prefs.dart'; -import 'package:watch_together/src/playback/media_marks.dart'; -import 'package:watch_together/src/screens/home/picked_media.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/jellyfin/session_media.dart'; +import 'package:starsync/src/jellyfin/track_prefs.dart'; +import 'package:starsync/src/playback/media_marks.dart'; +import 'package:starsync/src/screens/home/picked_media.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; // Two account holders and a guest watch Jellyfin episodes together over a // real sync server, real mpv and a local fake Jellyfin (never a real one): diff --git a/apps/desktop/test/jellyfin_session_test.dart b/apps/desktop/test/jellyfin_session_test.dart index 106996d..e57fe95 100644 --- a/apps/desktop/test/jellyfin_session_test.dart +++ b/apps/desktop/test/jellyfin_session_test.dart @@ -6,17 +6,17 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/jellyfin/browse_scope.dart'; -import 'package:watch_together/src/jellyfin/browser.dart'; -import 'package:watch_together/src/jellyfin/progress.dart'; -import 'package:watch_together/src/jellyfin/session_media.dart'; -import 'package:watch_together/src/jellyfin/track_prefs.dart'; -import 'package:watch_together/src/screens/home/media_picker.dart'; -import 'package:watch_together/src/screens/session/jellyfin_widgets.dart'; -import 'package:watch_together/src/session/delivery.dart'; -import 'package:watch_together/src/session/session_coordinator.dart' show AutoAdvance; -import 'package:watch_together/src/shell/library_tab.dart' show putUpJellyfinMedia; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/jellyfin/browse_scope.dart'; +import 'package:starsync/src/jellyfin/browser.dart'; +import 'package:starsync/src/jellyfin/progress.dart'; +import 'package:starsync/src/jellyfin/session_media.dart'; +import 'package:starsync/src/jellyfin/track_prefs.dart'; +import 'package:starsync/src/screens/home/media_picker.dart'; +import 'package:starsync/src/screens/session/jellyfin_widgets.dart'; +import 'package:starsync/src/session/delivery.dart'; +import 'package:starsync/src/session/session_coordinator.dart' show AutoAdvance; +import 'package:starsync/src/shell/library_tab.dart' show putUpJellyfinMedia; import 'jellyfin_accounts_test.dart'; import 'jellyfin_browser_test.dart' show app, setSize, settle; diff --git a/apps/desktop/test/library_rows_test.dart b/apps/desktop/test/library_rows_test.dart index 989c4a4..6e4310c 100644 --- a/apps/desktop/test/library_rows_test.dart +++ b/apps/desktop/test/library_rows_test.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/jellyfin/home_rows.dart'; -import 'package:watch_together/src/screens/home/media_picker.dart'; -import 'package:watch_together/src/shell/library_link.dart'; -import 'package:watch_together/src/shell/library_tab.dart'; +import 'package:starsync/src/jellyfin/home_rows.dart'; +import 'package:starsync/src/screens/home/media_picker.dart'; +import 'package:starsync/src/shell/library_link.dart'; +import 'package:starsync/src/shell/library_tab.dart'; final now = DateTime.utc(2026, 10, 8, 12); DateTime daysAgo(num d) => now.subtract(Duration(minutes: (d * 24 * 60).round())); diff --git a/apps/desktop/test/library_tab_test.dart b/apps/desktop/test/library_tab_test.dart index 4b86db2..a62a06e 100644 --- a/apps/desktop/test/library_tab_test.dart +++ b/apps/desktop/test/library_tab_test.dart @@ -5,12 +5,12 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/jellyfin/browse_scope.dart'; -import 'package:watch_together/src/jellyfin/details_panel.dart'; -import 'package:watch_together/src/screens/home/media_picker.dart'; -import 'package:watch_together/src/shell/library_link.dart'; -import 'package:watch_together/src/shell/library_tab.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/jellyfin/browse_scope.dart'; +import 'package:starsync/src/jellyfin/details_panel.dart'; +import 'package:starsync/src/screens/home/media_picker.dart'; +import 'package:starsync/src/shell/library_link.dart'; +import 'package:starsync/src/shell/library_tab.dart'; import 'jellyfin_accounts_test.dart' show FakeJellyfin, arifuretaId, fixtureServerId, jellyfinFixture, linkedAccounts, season2Id; diff --git a/apps/desktop/test/media_marks_test.dart b/apps/desktop/test/media_marks_test.dart index 9a47769..ef8de3b 100644 --- a/apps/desktop/test/media_marks_test.dart +++ b/apps/desktop/test/media_marks_test.dart @@ -1,8 +1,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/jellyfin/session_media.dart'; -import 'package:watch_together/src/jellyfin/track_prefs.dart'; -import 'package:watch_together/src/playback/media_marks.dart'; +import 'package:starsync/src/jellyfin/session_media.dart'; +import 'package:starsync/src/jellyfin/track_prefs.dart'; +import 'package:starsync/src/playback/media_marks.dart'; Duration s(num secs) => Duration(milliseconds: (secs * 1000).round()); Duration m(int min, [int sec = 0]) => Duration(minutes: min, seconds: sec); diff --git a/apps/desktop/test/minimal_controls_test.dart b/apps/desktop/test/minimal_controls_test.dart index abc789c..97b8a44 100644 --- a/apps/desktop/test/minimal_controls_test.dart +++ b/apps/desktop/test/minimal_controls_test.dart @@ -1,16 +1,16 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/controls/frame.dart'; -import 'package:watch_together/src/controls/minimal/style.dart'; -import 'package:watch_together/src/controls/minimal/timeline.dart'; -import 'package:watch_together/src/controls/model/controls_prefs.dart'; -import 'package:watch_together/src/controls/volume.dart'; -import 'package:watch_together/src/playback/media_marks.dart'; -import 'package:watch_together/src/shell/settings_tab.dart'; -import 'package:watch_together/src/state/settings.dart'; -import 'package:watch_together/src/update/update_controller.dart'; -import 'package:watch_together/src/widgets/app_menu.dart'; +import 'package:starsync/src/controls/frame.dart'; +import 'package:starsync/src/controls/minimal/style.dart'; +import 'package:starsync/src/controls/minimal/timeline.dart'; +import 'package:starsync/src/controls/model/controls_prefs.dart'; +import 'package:starsync/src/controls/volume.dart'; +import 'package:starsync/src/playback/media_marks.dart'; +import 'package:starsync/src/shell/settings_tab.dart'; +import 'package:starsync/src/state/settings.dart'; +import 'package:starsync/src/update/update_controller.dart'; +import 'package:starsync/src/widgets/app_menu.dart'; // The Minimal skin's pieces and the shared controls frame (never the // session screen or a media_kit `Video`). diff --git a/apps/desktop/test/notice_table_test.dart b/apps/desktop/test/notice_table_test.dart index 77e34e2..1581aca 100644 --- a/apps/desktop/test/notice_table_test.dart +++ b/apps/desktop/test/notice_table_test.dart @@ -9,8 +9,8 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart' show FeedMessage; -import 'package:watch_together/src/session/notifier.dart'; -import 'package:watch_together/src/sfx/sfx.dart'; +import 'package:starsync/src/session/notifier.dart'; +import 'package:starsync/src/sfx/sfx.dart'; final _at = DateTime.utc(2026, 1, 1); diff --git a/apps/desktop/test/now_playing_info_test.dart b/apps/desktop/test/now_playing_info_test.dart index 5a214d6..b939fdf 100644 --- a/apps/desktop/test/now_playing_info_test.dart +++ b/apps/desktop/test/now_playing_info_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/session/now_playing.dart'; -import 'package:watch_together/src/session/now_playing_info.dart'; +import 'package:starsync/src/session/now_playing.dart'; +import 'package:starsync/src/session/now_playing_info.dart'; void main() { group('jellyfinTitle', () { diff --git a/apps/desktop/test/player_controls_test.dart b/apps/desktop/test/player_controls_test.dart index adad2b0..51482bb 100644 --- a/apps/desktop/test/player_controls_test.dart +++ b/apps/desktop/test/player_controls_test.dart @@ -2,11 +2,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart' show JellyfinStreamType; -import 'package:watch_together/src/controls/control_menus.dart'; -import 'package:watch_together/src/controls/standard/seek_bar.dart'; -import 'package:watch_together/src/jellyfin/track_prefs.dart'; -import 'package:watch_together/src/playback/media_marks.dart'; -import 'package:watch_together/src/screens/session/player_controls.dart'; +import 'package:starsync/src/controls/control_menus.dart'; +import 'package:starsync/src/controls/standard/seek_bar.dart'; +import 'package:starsync/src/jellyfin/track_prefs.dart'; +import 'package:starsync/src/playback/media_marks.dart'; +import 'package:starsync/src/screens/session/player_controls.dart'; // The control-bar pieces alone (never the session screen or a `Video`): // the Standard seek bar, the skip pill and the shared track / chapter diff --git a/apps/desktop/test/player_settings_test.dart b/apps/desktop/test/player_settings_test.dart index 0420e5c..fd9eabc 100644 --- a/apps/desktop/test/player_settings_test.dart +++ b/apps/desktop/test/player_settings_test.dart @@ -6,18 +6,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/controls/frame.dart'; -import 'package:watch_together/src/controls/model/controls_prefs.dart'; -import 'package:watch_together/src/controls/player_settings.dart'; -import 'package:watch_together/src/jellyfin/track_prefs.dart'; -import 'package:watch_together/src/schedule/animeflow.dart'; -import 'package:watch_together/src/shaders/shader_presets.dart'; -import 'package:watch_together/src/shell/avatar.dart'; -import 'package:watch_together/src/shell/party_strip.dart'; -import 'package:watch_together/src/shell/settings_tab.dart'; -import 'package:watch_together/src/state/session_state.dart'; -import 'package:watch_together/src/state/settings.dart'; -import 'package:watch_together/src/update/update_controller.dart'; +import 'package:starsync/src/controls/frame.dart'; +import 'package:starsync/src/controls/model/controls_prefs.dart'; +import 'package:starsync/src/controls/player_settings.dart'; +import 'package:starsync/src/jellyfin/track_prefs.dart'; +import 'package:starsync/src/schedule/animeflow.dart'; +import 'package:starsync/src/shaders/shader_presets.dart'; +import 'package:starsync/src/shell/avatar.dart'; +import 'package:starsync/src/shell/party_strip.dart'; +import 'package:starsync/src/shell/settings_tab.dart'; +import 'package:starsync/src/state/session_state.dart'; +import 'package:starsync/src/state/settings.dart'; +import 'package:starsync/src/update/update_controller.dart'; // The player settings card (both skins, never a media_kit `Video`), the // Settings pages, the host flag in the party UI and invite addresses. diff --git a/apps/desktop/test/player_status_test.dart b/apps/desktop/test/player_status_test.dart index 74c93d9..af5cc67 100644 --- a/apps/desktop/test/player_status_test.dart +++ b/apps/desktop/test/player_status_test.dart @@ -5,16 +5,16 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/controls/frame.dart'; -import 'package:watch_together/src/controls/minimal/style.dart'; -import 'package:watch_together/src/controls/minimal/timeline.dart'; -import 'package:watch_together/src/session/health.dart'; -import 'package:watch_together/src/session/now_playing.dart'; -import 'package:watch_together/src/shell/now_playing_title.dart'; -import 'package:watch_together/src/shell/player_status.dart'; -import 'package:watch_together/src/shell/session_player.dart'; -import 'package:watch_together/src/shell/start_banner.dart'; -import 'package:watch_together/src/state/session_state.dart'; +import 'package:starsync/src/controls/frame.dart'; +import 'package:starsync/src/controls/minimal/style.dart'; +import 'package:starsync/src/controls/minimal/timeline.dart'; +import 'package:starsync/src/session/health.dart'; +import 'package:starsync/src/session/now_playing.dart'; +import 'package:starsync/src/shell/now_playing_title.dart'; +import 'package:starsync/src/shell/player_status.dart'; +import 'package:starsync/src/shell/session_player.dart'; +import 'package:starsync/src/shell/start_banner.dart'; +import 'package:starsync/src/state/session_state.dart'; // The player's title block, start card and status pill (never a media_kit // `Video`). diff --git a/apps/desktop/test/queue_logic_test.dart b/apps/desktop/test/queue_logic_test.dart index 531bb16..f763a01 100644 --- a/apps/desktop/test/queue_logic_test.dart +++ b/apps/desktop/test/queue_logic_test.dart @@ -2,7 +2,7 @@ // the janitor become server state. import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/queue/queue_logic.dart'; +import 'package:starsync/src/queue/queue_logic.dart'; MemberView member(String id, {bool stale = false, bool out = false, Map jf = const {}, String name = ''}) => diff --git a/apps/desktop/test/queue_panel_test.dart b/apps/desktop/test/queue_panel_test.dart index e4da791..671b158 100644 --- a/apps/desktop/test/queue_panel_test.dart +++ b/apps/desktop/test/queue_panel_test.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/queue/queue_ui.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; -import 'package:watch_together/src/shell/library_link.dart' show LinkPreview; -import 'package:watch_together/src/widgets/text_focus.dart'; +import 'package:starsync/src/queue/queue_ui.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; +import 'package:starsync/src/shell/library_link.dart' show LinkPreview; +import 'package:starsync/src/widgets/text_focus.dart'; void main() { testWidgets('Up next popover: holds the keys, Ctrl+V adds with a preview row, + Add link opens a field', diff --git a/apps/desktop/test/queue_switch_e2e_test.dart b/apps/desktop/test/queue_switch_e2e_test.dart index ea64c99..c5eda18 100644 --- a/apps/desktop/test/queue_switch_e2e_test.dart +++ b/apps/desktop/test/queue_switch_e2e_test.dart @@ -8,14 +8,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/jellyfin/session_media.dart'; -import 'package:watch_together/src/jellyfin/track_prefs.dart'; -import 'package:watch_together/src/playback/yt_dlp_app.dart'; -import 'package:watch_together/src/queue/queue_controller.dart' show WarmState; -import 'package:watch_together/src/screens/home/picked_media.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; -import 'package:watch_together/src/session/switch_trace.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/jellyfin/session_media.dart'; +import 'package:starsync/src/jellyfin/track_prefs.dart'; +import 'package:starsync/src/playback/yt_dlp_app.dart'; +import 'package:starsync/src/queue/queue_controller.dart' show WarmState; +import 'package:starsync/src/screens/home/picked_media.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; +import 'package:starsync/src/session/switch_trace.dart'; // How long an Up next switch takes, end to end, over real mpv and a real // sync server: two Jellyfin account holders and a guest (through the diff --git a/apps/desktop/test/readahead_e2e_test.dart b/apps/desktop/test/readahead_e2e_test.dart index f95d0f2..2ed0136 100644 --- a/apps/desktop/test/readahead_e2e_test.dart +++ b/apps/desktop/test/readahead_e2e_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/playback/playback_controller.dart'; +import 'package:starsync/src/playback/playback_controller.dart'; final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path; diff --git a/apps/desktop/test/relay_e2e_test.dart b/apps/desktop/test/relay_e2e_test.dart index ab421e5..6f4f05a 100644 --- a/apps/desktop/test/relay_e2e_test.dart +++ b/apps/desktop/test/relay_e2e_test.dart @@ -9,11 +9,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/jellyfin/track_prefs.dart'; -import 'package:watch_together/src/playback/yt_dlp_app.dart'; -import 'package:watch_together/src/screens/home/picked_media.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/jellyfin/track_prefs.dart'; +import 'package:starsync/src/playback/yt_dlp_app.dart'; +import 'package:starsync/src/screens/home/picked_media.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; // The relay with real coordinators, real mpv and a dedicated server that // has no file and no Jellyfin login of its own (the VPS setup): a guest's diff --git a/apps/desktop/test/release_name_test.dart b/apps/desktop/test/release_name_test.dart index 0cab1ab..95b732f 100644 --- a/apps/desktop/test/release_name_test.dart +++ b/apps/desktop/test/release_name_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/files/name_parser.dart'; +import 'package:starsync/src/files/name_parser.dart'; void _expect( String name, diff --git a/apps/desktop/test/schedule_anilist_test.dart b/apps/desktop/test/schedule_anilist_test.dart index 1526c88..c56c1e9 100644 --- a/apps/desktop/test/schedule_anilist_test.dart +++ b/apps/desktop/test/schedule_anilist_test.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/schedule/anilist.dart'; -import 'package:watch_together/src/session/media_art.dart'; +import 'package:starsync/src/schedule/anilist.dart'; +import 'package:starsync/src/session/media_art.dart'; import 'schedule_fake_server.dart'; @@ -45,7 +45,7 @@ void main() { cacheDir: () async => cache, delay: (d) async => delays.add(d), now: () => now, - userAgent: 'watch-together/test', + userAgent: 'Starsync/test', ); setUp(() async { @@ -73,7 +73,7 @@ void main() { expect(server.seen, hasLength(2)); final first = jsonDecode(server.seen.first.body) as Map; expect(first['variables'], {'from': from.millisecondsSinceEpoch ~/ 1000 - 1, 'to': to.millisecondsSinceEpoch ~/ 1000, 'page': 1}); - expect(server.seen.first.headers.value(HttpHeaders.userAgentHeader), 'watch-together/test'); + expect(server.seen.first.headers.value(HttpHeaders.userAgentHeader), 'Starsync/test'); expect(s.airings.map((a) => a.anilistId), [1, 4]); final a = s.airings.first; expect(a.title, 'Show'); diff --git a/apps/desktop/test/schedule_animeflow_test.dart b/apps/desktop/test/schedule_animeflow_test.dart index 507ac4d..c06c356 100644 --- a/apps/desktop/test/schedule_animeflow_test.dart +++ b/apps/desktop/test/schedule_animeflow_test.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/schedule/animeflow.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/schedule/animeflow.dart'; import 'schedule_fake_server.dart'; @@ -83,7 +83,7 @@ void main() { final c = client(); final me = await c.me(); expect(server.seen.single.headers.value('x-emby-token'), token); - expect(server.seen.single.headers.value(HttpHeaders.userAgentHeader), startsWith('watch-together/')); + expect(server.seen.single.headers.value(HttpHeaders.userAgentHeader), startsWith('Starsync/')); expect(me.userId, 'u1'); expect(me.name, 'Foxyz'); expect(me.scopes, {AnimeFlowScope.view, AnimeFlowScope.request}); diff --git a/apps/desktop/test/schedule_model_test.dart b/apps/desktop/test/schedule_model_test.dart index ceac49a..9c662e5 100644 --- a/apps/desktop/test/schedule_model_test.dart +++ b/apps/desktop/test/schedule_model_test.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/schedule/anilist.dart'; -import 'package:watch_together/src/schedule/animeflow.dart'; -import 'package:watch_together/src/schedule/schedule_model.dart'; +import 'package:starsync/src/schedule/anilist.dart'; +import 'package:starsync/src/schedule/animeflow.dart'; +import 'package:starsync/src/schedule/schedule_model.dart'; import 'schedule_fake_server.dart'; diff --git a/apps/desktop/test/schedule_page_test.dart b/apps/desktop/test/schedule_page_test.dart index 5f6e3f1..0710a5c 100644 --- a/apps/desktop/test/schedule_page_test.dart +++ b/apps/desktop/test/schedule_page_test.dart @@ -6,13 +6,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/schedule/anilist.dart'; -import 'package:watch_together/src/schedule/animeflow.dart'; -import 'package:watch_together/src/schedule/schedule_model.dart'; -import 'package:watch_together/src/schedule/ui/schedule_actions.dart'; -import 'package:watch_together/src/schedule/ui/schedule_page.dart'; -import 'package:watch_together/src/screens/home/picked_media.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/schedule/anilist.dart'; +import 'package:starsync/src/schedule/animeflow.dart'; +import 'package:starsync/src/schedule/schedule_model.dart'; +import 'package:starsync/src/schedule/ui/schedule_actions.dart'; +import 'package:starsync/src/schedule/ui/schedule_page.dart'; +import 'package:starsync/src/screens/home/picked_media.dart'; import 'schedule_fake_server.dart'; diff --git a/apps/desktop/test/seek_drag_e2e_test.dart b/apps/desktop/test/seek_drag_e2e_test.dart index 20b7136..f2b231a 100644 --- a/apps/desktop/test/seek_drag_e2e_test.dart +++ b/apps/desktop/test/seek_drag_e2e_test.dart @@ -5,8 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; -import 'package:watch_together/src/sync/state_diff.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; +import 'package:starsync/src/sync/state_diff.dart'; final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path; diff --git a/apps/desktop/test/server_isolate_test.dart b/apps/desktop/test/server_isolate_test.dart index 5ed6751..7458abe 100644 --- a/apps/desktop/test/server_isolate_test.dart +++ b/apps/desktop/test/server_isolate_test.dart @@ -9,11 +9,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive_ce/hive.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/jellyfin/accounts.dart'; -import 'package:watch_together/src/screens/home/picked_media.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; -import 'package:watch_together/src/state/server_isolate.dart'; -import 'package:watch_together/src/state/session_state.dart'; +import 'package:starsync/src/jellyfin/accounts.dart'; +import 'package:starsync/src/screens/home/picked_media.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; +import 'package:starsync/src/state/server_isolate.dart'; +import 'package:starsync/src/state/session_state.dart'; Future _poll( FutureOr Function() cond, diff --git a/apps/desktop/test/session_logic_test.dart b/apps/desktop/test/session_logic_test.dart index 2766755..0f925cd 100644 --- a/apps/desktop/test/session_logic_test.dart +++ b/apps/desktop/test/session_logic_test.dart @@ -2,8 +2,8 @@ // IdleTracker and unreadyForNewMedia give way to auto sit-out; // linkDisplayName stays. import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/session/idle.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; +import 'package:starsync/src/session/idle.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; void main() { group('IdleTracker', () { diff --git a/apps/desktop/test/settings_schema_test.dart b/apps/desktop/test/settings_schema_test.dart new file mode 100644 index 0000000..bbaf5e2 --- /dev/null +++ b/apps/desktop/test/settings_schema_test.dart @@ -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 = []; + 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); + }); + }); +} diff --git a/apps/desktop/test/sfx_test.dart b/apps/desktop/test/sfx_test.dart index d52a645..89f31d6 100644 --- a/apps/desktop/test/sfx_test.dart +++ b/apps/desktop/test/sfx_test.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/sfx/sfx.dart'; -import 'package:watch_together/src/sfx/sound_bank.dart'; -import 'package:watch_together/src/sfx/synth.dart'; +import 'package:starsync/src/sfx/sfx.dart'; +import 'package:starsync/src/sfx/sound_bank.dart'; +import 'package:starsync/src/sfx/synth.dart'; double _pitch(List x, int from) { final c = []; diff --git a/apps/desktop/test/shaders_mpv_test.dart b/apps/desktop/test/shaders_mpv_test.dart index d0da2e6..8a40507 100644 --- a/apps/desktop/test/shaders_mpv_test.dart +++ b/apps/desktop/test/shaders_mpv_test.dart @@ -2,10 +2,10 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; -import 'package:watch_together/src/playback/playback_controller.dart'; -import 'package:watch_together/src/shaders/shader_assets.dart'; -import 'package:watch_together/src/shaders/shader_presets.dart'; -import 'package:watch_together/src/state/settings.dart'; +import 'package:starsync/src/playback/playback_controller.dart'; +import 'package:starsync/src/shaders/shader_assets.dart'; +import 'package:starsync/src/shaders/shader_presets.dart'; +import 'package:starsync/src/state/settings.dart'; final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path; diff --git a/apps/desktop/test/shaders_test.dart b/apps/desktop/test/shaders_test.dart index 8d7e9da..78d8f0c 100644 --- a/apps/desktop/test/shaders_test.dart +++ b/apps/desktop/test/shaders_test.dart @@ -2,9 +2,9 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive_ce/hive.dart'; -import 'package:watch_together/src/shaders/shader_assets.dart'; -import 'package:watch_together/src/shaders/shader_presets.dart'; -import 'package:watch_together/src/state/settings.dart'; +import 'package:starsync/src/shaders/shader_assets.dart'; +import 'package:starsync/src/shaders/shader_presets.dart'; +import 'package:starsync/src/state/settings.dart'; final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path; diff --git a/apps/desktop/test/shell_test.dart b/apps/desktop/test/shell_test.dart index 82aa209..094c325 100644 --- a/apps/desktop/test/shell_test.dart +++ b/apps/desktop/test/shell_test.dart @@ -4,18 +4,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/screens/home/profile.dart'; -import 'package:watch_together/src/shell/app_shell.dart'; -import 'package:watch_together/src/shell/avatar.dart'; -import 'package:watch_together/src/shell/motion.dart'; -import 'package:watch_together/src/shell/party_strip.dart'; -import 'package:watch_together/src/shell/profile_card.dart'; -import 'package:watch_together/src/shell/session_player.dart'; -import 'package:watch_together/src/shell/settings_tab.dart' show SettingsTab; -import 'package:watch_together/src/shell/toasts.dart'; -import 'package:watch_together/src/session/session_coordinator.dart' show ToastItem; -import 'package:watch_together/src/state/session_state.dart'; -import 'package:watch_together/src/update/update_controller.dart'; +import 'package:starsync/src/screens/home/profile.dart'; +import 'package:starsync/src/shell/app_shell.dart'; +import 'package:starsync/src/shell/avatar.dart'; +import 'package:starsync/src/shell/motion.dart'; +import 'package:starsync/src/shell/party_strip.dart'; +import 'package:starsync/src/shell/profile_card.dart'; +import 'package:starsync/src/shell/session_player.dart'; +import 'package:starsync/src/shell/settings_tab.dart' show SettingsTab; +import 'package:starsync/src/shell/toasts.dart'; +import 'package:starsync/src/session/session_coordinator.dart' show ToastItem; +import 'package:starsync/src/state/session_state.dart'; +import 'package:starsync/src/update/update_controller.dart'; /// Stands in for media_kit's `Video` (which hangs flutter_test) and /// counts how often one is created. diff --git a/apps/desktop/test/sound_device_e2e_test.dart b/apps/desktop/test/sound_device_e2e_test.dart index fda3da7..2131998 100644 --- a/apps/desktop/test/sound_device_e2e_test.dart +++ b/apps/desktop/test/sound_device_e2e_test.dart @@ -3,10 +3,10 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/platform/audio/sound_engine.dart'; -import 'package:watch_together/src/sfx/sfx.dart'; -import 'package:watch_together/src/sfx/sound_bank.dart'; -import 'package:watch_together/src/sfx/synth_sfx.dart'; +import 'package:starsync/src/platform/audio/sound_engine.dart'; +import 'package:starsync/src/sfx/sfx.dart'; +import 'package:starsync/src/sfx/sound_bank.dart'; +import 'package:starsync/src/sfx/synth_sfx.dart'; // Needs a real audio output: opens the device through flutter_soloud and // plays every sound of every preset once, quietly (WT_SFX_VOLUME, default diff --git a/apps/desktop/test/sound_engine_test.dart b/apps/desktop/test/sound_engine_test.dart index 2ccbc42..90c1130 100644 --- a/apps/desktop/test/sound_engine_test.dart +++ b/apps/desktop/test/sound_engine_test.dart @@ -1,11 +1,11 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/platform/audio/sound_engine.dart'; -import 'package:watch_together/src/sfx/sfx.dart'; -import 'package:watch_together/src/sfx/sound_bank.dart'; -import 'package:watch_together/src/sfx/synth.dart'; -import 'package:watch_together/src/sfx/synth_sfx.dart'; +import 'package:starsync/src/platform/audio/sound_engine.dart'; +import 'package:starsync/src/sfx/sfx.dart'; +import 'package:starsync/src/sfx/sound_bank.dart'; +import 'package:starsync/src/sfx/synth.dart'; +import 'package:starsync/src/sfx/synth_sfx.dart'; class _FakeBackend implements AudioBackend { final bool failInit; diff --git a/apps/desktop/test/state_diff_test.dart b/apps/desktop/test/state_diff_test.dart index daf6bae..67e065e 100644 --- a/apps/desktop/test/state_diff_test.dart +++ b/apps/desktop/test/state_diff_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/sync/state_diff.dart'; +import 'package:starsync/src/sync/state_diff.dart'; StateBroadcast _state({ int eventId = 0, diff --git a/apps/desktop/test/sync_playback_e2e_test.dart b/apps/desktop/test/sync_playback_e2e_test.dart index ee04371..335488e 100644 --- a/apps/desktop/test/sync_playback_e2e_test.dart +++ b/apps/desktop/test/sync_playback_e2e_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; const _port = 18080; const _secret = 'e2e-secret'; diff --git a/apps/desktop/test/text_focus_test.dart b/apps/desktop/test/text_focus_test.dart index c71a9e4..0c6c704 100644 --- a/apps/desktop/test/text_focus_test.dart +++ b/apps/desktop/test/text_focus_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:watch_together/src/widgets/text_focus.dart'; +import 'package:starsync/src/widgets/text_focus.dart'; void main() { // Regression: the old FocusNode-parent walk never met EditableText, so diff --git a/apps/desktop/test/update_controller_test.dart b/apps/desktop/test/update_controller_test.dart index 06ee026..fe7bfd4 100644 --- a/apps/desktop/test/update_controller_test.dart +++ b/apps/desktop/test/update_controller_test.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/update/update_controller.dart'; +import 'package:starsync/src/update/update_controller.dart'; // Work dir base: WT_TEST_TMP or the system temp. Staging lives inside // the install root, so the result doesn't depend on which filesystem @@ -24,49 +24,39 @@ final _posixOnly = Platform.isWindows ? 'POSIX install layout' : null; bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0; // Builds the release archive exactly like tool/package_linux.sh: -// bundle copied into /watch-together-/, then -// `tar -czf out -C watch-together-`. [zip]: like -// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only). +// bundle copied into /starsync-/ with the launcher by the +// old name (lib/src/update/bundle/watch_together, the shipped file), then +// `tar -czf out -C starsync-`. [app]: the app binary's +// content. Future> _releaseTarball(Directory work, - {String version = '0.2.0', - bool windows = false, - bool bundle = true, - bool zip = false}) async { - final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}'); - final folder = Directory('${stage.path}/watch-together-$version'); + {String version = '0.2.0', String app = 'binary-v2'}) async { + final stage = Directory('${work.path}/pkg-$version-lin'); + final folder = Directory('${stage.path}/starsync-$version'); await Directory('${folder.path}/data/flutter_assets').create(recursive: true); await File('${folder.path}/data/flutter_assets/fresh.cfg') .writeAsString('fresh-config'); - if (windows) { - await File('${folder.path}/watch_together.exe').writeAsString('binary-v2'); - await File('${folder.path}/flutter_windows.dll').writeAsString('dll-v2'); - } else { - await File('${folder.path}/watch_together').writeAsString('binary-v2'); - if (bundle) { - await Directory('${folder.path}/lib').create(); - await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2'); - await Link('${folder.path}/lib/libapp.so').create('libapp.so.1'); - } - await File('${folder.path}/update.sh') - .writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n'); - if (!Platform.isWindows) { - final r = await Process.run('chmod', - ['755', '${folder.path}/watch_together', '${folder.path}/update.sh']); - expect(r.exitCode, 0); - } + await File('${folder.path}/starsync').writeAsString(app); + await Directory('${folder.path}/lib').create(); + await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2'); + await Link('${folder.path}/lib/libapp.so').create('libapp.so.1'); + for (final f in ['update.sh', 'watch_together', 'watch-together-server']) { + await File('lib/src/update/bundle/$f').copy('${folder.path}/$f'); } - final out = File('${work.path}/out-$version.${zip ? 'zip' : 'tar.gz'}'); - final r = await Process.run('tar', [ - ...zip ? ['-a', '-c', '-f'] : ['-czf'], - out.path, - '-C', - stage.path, - 'watch-together-$version', - ]); + if (!Platform.isWindows) { + final r = await Process.run('chmod', [ + '755', + for (final f in ['starsync', 'update.sh', 'watch_together', 'watch-together-server']) + '${folder.path}/$f' + ]); + expect(r.exitCode, 0); + } + final out = File('${work.path}/out-$version.tar.gz'); + final r = await Process.run('tar', ['-czf', out.path, '-C', stage.path, 'starsync-$version']); expect(r.exitCode, 0, reason: '${r.stderr}'); return out.readAsBytes(); } +// An install as 1.2.x left it: the binary is still called watch_together. Future _fakeRoot(Directory work, {bool windows = false}) async { final root = Directory('${work.path}/install'); await Directory('${root.path}/data').create(recursive: true); @@ -105,12 +95,12 @@ class _Server { req.response.write(jsonEncode({ 'version': '0.2.0', 'protocol': RelayConstants.protoVersion, - 'url': '$base/watch-together-0.2.0-linux-x64.tar.gz', + 'url': '$base/starsync-0.2.0-linux-x64.tar.gz', 'sha256': sha256.convert(tarball).toString(), 'notes': 'test build', ...manifestExtra, })); - } else if (req.uri.path == '/watch-together-0.2.0-linux-x64.tar.gz') { + } else if (req.uri.path == '/starsync-0.2.0-linux-x64.tar.gz') { tarHits++; req.response.contentLength = tarball.length; req.response.add(tarball); @@ -145,12 +135,14 @@ void main() { test('check, download with progress, apply, restart same binary', () async { final root = await _fakeRoot(work); + // Already renamed: started as starsync. + await File('${root.path}/watch_together').rename('${root.path}/starsync'); final restarts = <(String, List)>[]; launchArgs = const ['--some-flag']; final c = UpdateController( currentVersion: '0.1.0', manifestUrl: server.manifestUrl, - executablePath: '${root.path}/watch_together', + executablePath: '${root.path}/starsync', restart: (exe, args) async => restarts.add((exe, args))); await c.checkOnce(); expect(c.status.value.phase, UpdatePhase.available); @@ -164,13 +156,35 @@ void main() { await c.install(); expect(progress.last, 1.0); expect(c.status.value.phase, UpdatePhase.restarting); - expect(await File('${root.path}/watch_together').readAsString(), + expect(await File('${root.path}/starsync').readAsString(), 'binary-v2'); - expect(_isExec('${root.path}/watch_together'), isTrue); - expect(restarts.single.$1, '${root.path}/watch_together'); + expect(_isExec('${root.path}/starsync'), isTrue); + expect(restarts.single.$1, '${root.path}/starsync'); expect(restarts.single.$2, ['--some-flag']); }, skip: _posixOnly); + test('started by the old name: restarts the new binary; the old name launches it', + () async { + server.tarball = await _releaseTarball(work, + version: '0.2.1', app: '#!/bin/sh\necho "starsync \$*"\n'); + final root = await _fakeRoot(work); + final restarts = []; + final c = UpdateController( + currentVersion: '0.1.0', + manifestUrl: server.manifestUrl, + executablePath: '${root.path}/watch_together', + restart: (exe, _) async => restarts.add(exe)); + await c.checkOnce(); + await c.install(); + expect(c.status.value.phase, UpdatePhase.restarting); + expect(restarts.single, '${root.path}/starsync'); + // What a 1.2.x app restarts after this update, and the VPS server + // name: both launch the new binaries. + final app = await Process.run('${root.path}/watch_together', ['--a']); + expect('${app.stdout}'.trim(), 'starsync --a', reason: '${app.stderr}'); + expect(_isExec('${root.path}/watch-together-server'), isTrue); + }, skip: _posixOnly); + test('newer protocol is a required update, cannot be dismissed', () async { server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1}; diff --git a/apps/desktop/test/user_seek_test.dart b/apps/desktop/test/user_seek_test.dart index 8c8130c..19c6368 100644 --- a/apps/desktop/test/user_seek_test.dart +++ b/apps/desktop/test/user_seek_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sync_relay_core/core.dart'; -import 'package:watch_together/src/sync/user_seek.dart'; +import 'package:starsync/src/sync/user_seek.dart'; StateBroadcast _state({int eventId = 1, String? seekBy, int? seekSeq}) => StateBroadcast( diff --git a/apps/desktop/test/video_reload_e2e_test.dart b/apps/desktop/test/video_reload_e2e_test.dart index 0b22bf6..211a22a 100644 --- a/apps/desktop/test/video_reload_e2e_test.dart +++ b/apps/desktop/test/video_reload_e2e_test.dart @@ -6,8 +6,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; -import 'package:watch_together/src/session/session_coordinator.dart'; -import 'package:watch_together/src/sync/state_diff.dart'; +import 'package:starsync/src/session/session_coordinator.dart'; +import 'package:starsync/src/sync/state_diff.dart'; const _mediaPath = '/home/foxyz/Videos/chiyo.mp4'; diff --git a/apps/desktop/test/volume_readout_test.dart b/apps/desktop/test/volume_readout_test.dart index e3c9c4d..8c3671c 100644 --- a/apps/desktop/test/volume_readout_test.dart +++ b/apps/desktop/test/volume_readout_test.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart' show VideoState; -import 'package:watch_together/src/controls/model/controls_prefs.dart'; -import 'package:watch_together/src/controls/frame.dart'; -import 'package:watch_together/src/shell/session_player.dart'; -import 'package:watch_together/src/state/session_state.dart'; -import 'package:watch_together/src/state/settings.dart'; +import 'package:starsync/src/controls/model/controls_prefs.dart'; +import 'package:starsync/src/controls/frame.dart'; +import 'package:starsync/src/shell/session_player.dart'; +import 'package:starsync/src/state/session_state.dart'; +import 'package:starsync/src/state/settings.dart'; // The wheel anywhere on the full player flashes the volume readout, in // both skins, windowed and fullscreen, with the controls up or hidden. diff --git a/apps/desktop/test/ytdl_headers_e2e_test.dart b/apps/desktop/test/ytdl_headers_e2e_test.dart index 8e843c0..0155cea 100644 --- a/apps/desktop/test/ytdl_headers_e2e_test.dart +++ b/apps/desktop/test/ytdl_headers_e2e_test.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:media_kit/media_kit.dart'; -import 'package:watch_together/src/playback/playback_controller.dart'; +import 'package:starsync/src/playback/playback_controller.dart'; final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path; const _referer = 'https://www.bilibili.com/'; diff --git a/apps/desktop/windows/CMakeLists.txt b/apps/desktop/windows/CMakeLists.txt index 733de55..40549fd 100644 --- a/apps/desktop/windows/CMakeLists.txt +++ b/apps/desktop/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(watch_together LANGUAGES CXX) +project(starsync LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "watch_together") +set(BINARY_NAME "starsync") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/apps/desktop/windows/runner/Runner.rc b/apps/desktop/windows/runner/Runner.rc index fb8e923..7a7a3b2 100644 --- a/apps/desktop/windows/runner/Runner.rc +++ b/apps/desktop/windows/runner/Runner.rc @@ -89,13 +89,13 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "watch_together" "\0" + VALUE "CompanyName", "foxyz" "\0" + VALUE "FileDescription", "Starsync" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "watch_together" "\0" - VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "watch_together.exe" "\0" - VALUE "ProductName", "watch_together" "\0" + VALUE "InternalName", "Starsync" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 foxyz. All rights reserved." "\0" + VALUE "OriginalFilename", "starsync.exe" "\0" + VALUE "ProductName", "Starsync" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/apps/desktop/windows/runner/win32_window.cpp b/apps/desktop/windows/runner/win32_window.cpp index 60608d0..c55ed46 100644 --- a/apps/desktop/windows/runner/win32_window.cpp +++ b/apps/desktop/windows/runner/win32_window.cpp @@ -16,7 +16,7 @@ namespace { #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; +constexpr const wchar_t kWindowClassName[] = L"STARSYNC_WINDOW"; /// Registry key for app theme preference. /// diff --git a/deploy/watch-together.service b/deploy/starsync.service similarity index 52% rename from deploy/watch-together.service rename to deploy/starsync.service index 8d0d7f6..1132adc 100644 --- a/deploy/watch-together.service +++ b/deploy/starsync.service @@ -1,7 +1,10 @@ -# Dedicated watch-together server (no display needed). -# Install: unpack the release tarball to /opt/watch-together (the unit only -# uses the watch-together-server binary in it), copy this file to -# /etc/systemd/system/, then `systemctl enable --now watch-together`. +# Dedicated Starsync server (no display needed). +# Install: unpack the release tarball to /opt/starsync (the unit only uses +# the starsync-server binary in it), copy this file to +# /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 (default 8080), --secret (empty = open # server, anyone who can reach the port can join), --name, --media , # --advertise-host , --no-yt-dlp, --yt-dlp-dir , --config @@ -11,15 +14,15 @@ # process lists are readable by other users. [Unit] -Description=watch-together dedicated server +Description=Starsync dedicated server After=network-online.target Wants=network-online.target [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 DynamicUser=true -StateDirectory=watch-together +StateDirectory=starsync NoNewPrivileges=true [Install] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index eed563e..4290e30 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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/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 | | `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 asking. - **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`, `--jellyfin `, `--[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") @@ -1174,6 +1174,10 @@ handed on from there (the coordinator gets its `Notifier`, `Connection` and options, warm-up. - `SystemNotifications`: an interface only; desktop shows none (`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 `AppYtDlp.instance.mpvRawOptions`), the updater's restart (`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 fails) and `settings`. Where Hive and the logs live is `AppDirs` -(`platform/app_dirs.dart`, resolved once in `main`): Hive's default folder -(the documents folder) and `/logs`; with `STARSYNC_PROFILE` -(DEVELOPMENT "Dev loop") `/profiles//hive` and `logs`, -and the window title gets ` []`. yt-dlp, shaders and caches stay -shared. Link media names are a web page's title (see "yt-dlp"), else +(`platform/ports/app_dirs.dart`, resolved once in `main` by the +`AppDirsPort`, `DesktopAppDirs` on the desktop): `/hive` and +`/logs`; with `STARSYNC_PROFILE` (DEVELOPMENT "Dev loop") +`/profiles//hive` and `logs`, and the window title gets +` []`. 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 +`/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 +`/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. 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`). - **Where**: the app writes `/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 `/profiles//logs`, see `AppDirs` under "Desktop UI"); its hosted server's records arrive over the isolate's `events` port and go in the same file as `server.` (`relogLine`). The dedicated server @@ -1809,13 +1840,19 @@ status. `assets/icon/icon.svg` is the source; `icon_.png` and the Windows `app_icon.ico` are rendered from it (rsvg-convert, magick). The GTK -application id is `moe.senkochan.watch_together`, which Wayland uses as the -window's `app_id`; compositors take the icon from the `.desktop` file of -that id. A tarball install has none, so on start a Linux release build -(`platform/desktop_entry.dart`, skipped in debug) writes -`~/.local/share/applications/moe.senkochan.watch_together.desktop` (Exec = -the running binary) and the icons into the hicolor theme, only when they -changed. The runner also sets the window icon from the bundle (X11). +application id is `dev.foxyz.starsync` (also the X11 `WM_CLASS`, from +`g_set_prgname`), which Wayland uses as the window's `app_id`; compositors +take the icon from the `.desktop` file of that id. A tarball install has +none, so on start a Linux release build (`platform/desktop_entry.dart`, +skipped in debug) writes +`~/.local/share/applications/dev.foxyz.starsync.desktop` (Exec = the +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 @@ -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, with rollback on failure (writing the running executable fails with ETXTBSY; 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 -manually". A manifest whose +the binary (app or server, same folder; Windows: `data/` and +`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--`, +top folder `starsync-/`) 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 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` / @@ -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 uncommitted changes, passed by `tool/package_linux.sh`), and for a dev 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; -it ends with "restart the server to run ". `watch_together --upgrade` +it ends with "restart the server to run ". `starsync --upgrade` does the same from the app binary. In the app, `UpdateController` (`apps/desktop/lib/src/update/`) runs the same code for the update bar (`UpdateBar`, `screens/home/update_indicator.dart`): a band across the diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index b2dd574..597215a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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= tool/package_linux.sh [--skip-build] [--noupload] # dist/*.tar.gz powershell -File tool/package_windows.ps1 [-SkipBuild] # dist/*.zip (on Windows) -cd packages/server && dart compile exe --define=APP_VERSION= bin/server.dart -o watch-together-server +cd packages/server && dart compile exe --define=APP_VERSION= bin/server.dart -o starsync-server ``` `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 tarball to Discord (never commit it). The packager always compiles the 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--linux-x64.tar.gz` / `-windows-x64.zip` with one +top folder `starsync-/`; 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 @@ -224,8 +230,8 @@ unexpectedly` line just before it is expected. Without the app: After a session, read the log files rather than reproducing ("Logging" in [ARCHITECTURE.md](ARCHITECTURE.md) has what goes in them): -the app's are `~/.local/share/moe.senkochan.watch_together/logs/app-*.log` -on Linux (on Windows under `%APPDATA%`; Settings → About → "Open logs +the app's are `~/.local/share/dev.foxyz.starsync/logs/app-*.log` +on Linux (on Windows `%APPDATA%\foxyz\Starsync\logs`; Settings → About → "Open logs folder" finds it anywhere), with the hosted server's lines in the same file; a dedicated server prints its folder at start (`--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 `GDK_BACKEND` / `FLUTTER_LINUX_RENDERER` override). Its reason is only on 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 is off), not software rendering. diff --git a/docs/HOW-IT-WORKS.md b/docs/HOW-IT-WORKS.md index b9359d0..43df2f2 100644 --- a/docs/HOW-IT-WORKS.md +++ b/docs/HOW-IT-WORKS.md @@ -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. 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. Friends join either way with Join a friend in the strip at the top of the diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 06493cb..5a7b881 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -85,15 +85,6 @@ the automatic continuation and countdown, relay state), the device (client id, library and folders, match corrections, watch history, settings, notification prefs, what this app is sourcing) and the 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 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 @@ -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 friends stay on 1.2.x until the 1.3.0 release (Windows-only fixes like P30 reach them then). -Rename to Starsync everywhere (repo, app id, data folder, updater URL -migration) is its own step; app id `dev.foxyz.starsync`, Windows -company name `foxyz` (replacing Flutter's default `com.example`). Auto +Left of the rename (app, binaries, archives and data folder are +Starsync): the repository (`Foxyz/sync_replay_project`) and with it the +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 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 uncleanly and resumes" fails about half the time under the full suite. -6. **Windows: first real use.** CI builds and publishes - `watch-together--windows-x64.zip` (libmpv bundled) and runs the +6. **Windows: first real use.** Check the rename at 1.3.0: a 1.2.x + 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--windows-x64.zip` (libmpv bundled) and runs the updater tests on NTFS; a friend has joined from it. Still unchecked: fullscreen controls, hosting (firewall prompt), in-app update from one release to the next. The rest of the desktop tests are not in CI. diff --git a/packages/core/lib/src/diag_share.dart b/packages/core/lib/src/diag_share.dart index 7a6d63d..05edbd1 100644 --- a/packages/core/lib/src/diag_share.dart +++ b/packages/core/lib/src/diag_share.dart @@ -142,7 +142,7 @@ typedef DiagTarget = ({Uri base, String token, double offsetMs}); /// saying so per connection; problem marks ([markProblem]) go either way. class DiagUploader { 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 int bufferBytes; final int batchBytes; diff --git a/packages/core/lib/src/updater.dart b/packages/core/lib/src/updater.dart index d1a7781..639fb80 100644 --- a/packages/core/lib/src/updater.dart +++ b/packages/core/lib/src/updater.dart @@ -7,9 +7,9 @@ import 'package:crypto/crypto.dart'; import 'constants.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 -// 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 // 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. 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 _backupDir = '.update-backup'; const _downloadFile = '.update-download'; @@ -303,7 +338,7 @@ List _safeSegments(String name) { } // Release archives wrap the bundle in one top-level folder -// (watch-together-/, see tool/package_linux.sh). Strip it; a flat +// (starsync-/, see tool/package_linux.sh). Strip it; a flat // archive (data/, lib/ and the binary at the top) is used as is. int _wrapperDepth(List> entries) { final tops = entries.where((s) => s.isNotEmpty).map((s) => s.first).toSet(); @@ -414,7 +449,7 @@ Future applyUpdate(File archive, Directory root, await _extract(await archive.readAsBytes(), stage, !win); if (!_isBundle(stage.path, win) || (executableName != null && - !await File('${stage.path}/$executableName').exists())) { + !await _hasBinary(stage.path, executableName))) { throw const FormatException( 'archive is not a release bundle (missing data/, lib/ or binary)'); } @@ -428,6 +463,14 @@ Future 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 _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 { final String name; final bool hadOld; @@ -565,7 +608,7 @@ Future runUpgrade( HttpClient? http, String? executablePath, Directory? installRoot, - String program = 'watch-together', + String program = 'starsync', String Function(String version)? restartNote, bool checkOnly = false}) async { stdout.writeln('$program $currentVersion (proto ${RelayConstants.protoVersion})'); diff --git a/packages/core/test/updater_test.dart b/packages/core/test/updater_test.dart index db24f49..324048a 100644 --- a/packages/core/test/updater_test.dart +++ b/packages/core/test/updater_test.dart @@ -24,34 +24,48 @@ final _posixOnly = Platform.isWindows ? 'POSIX install layout' : null; bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0; // Builds the release archive exactly like tool/package_linux.sh: -// bundle copied into /watch-together-/, then -// `tar -czf out -C watch-together-`. [zip]: like +// bundle copied into /starsync-/, then +// `tar -czf out -C starsync-`. [zip]: like // 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> _releaseTarball(Directory work, {String version = '0.2.0', bool windows = false, 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 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 File('${folder.path}/data/flutter_assets/fresh.cfg') .writeAsString('fresh-config'); if (windows) { - await File('${folder.path}/watch_together.exe').writeAsString('binary-v2'); + await File('${folder.path}/starsync.exe').writeAsString('binary-v2'); + if (legacy) { + 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'); + await File('${folder.path}/starsync').writeAsString(app); if (bundle) { await Directory('${folder.path}/lib').create(); await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2'); await Link('${folder.path}/lib/libapp.so').create('libapp.so.1'); } await File('${folder.path}/update.sh') - .writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n'); + .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) { - final r = await Process.run('chmod', - ['755', '${folder.path}/watch_together', '${folder.path}/update.sh']); + final r = await Process.run( + 'chmod', ['755', for (final f in exec) '${folder.path}/$f']); expect(r.exitCode, 0); } } @@ -61,12 +75,13 @@ Future> _releaseTarball(Directory work, out.path, '-C', stage.path, - 'watch-together-$version', + 'starsync-$version', ]); expect(r.exitCode, 0, reason: '${r.stderr}'); return out.readAsBytes(); } +// An install as 1.2.x left it: the binary is still called watch_together. Future _fakeRoot(Directory work, {bool windows = false}) async { final root = Directory('${work.path}/install'); await Directory('${root.path}/data').create(recursive: true); @@ -111,12 +126,12 @@ class _Server { req.response.write(jsonEncode({ 'version': '0.2.0', 'protocol': RelayConstants.protoVersion, - 'url': '$base/watch-together-0.2.0-linux-x64.tar.gz', + 'url': '$base/starsync-0.2.0-linux-x64.tar.gz', 'sha256': sha256.convert(tarball).toString(), 'notes': 'test build', ...manifestExtra, })); - } else if (req.uri.path == '/watch-together-0.2.0-linux-x64.tar.gz') { + } else if (req.uri.path == '/starsync-0.2.0-linux-x64.tar.gz') { tarHits++; req.response.contentLength = tarball.length; req.response.add(tarball); @@ -262,11 +277,12 @@ void main() { expect(rc, 0); expect(server.tarHits, 1); 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(), 'fresh-config'); - expect(Directory('$p/watch-together-0.2.0').existsSync(), isFalse); - expect(_isExec('$p/watch_together'), isTrue); + expect(Directory('$p/starsync-0.2.0').existsSync(), isFalse); + expect(_isExec('$p/starsync'), isTrue); + expect(_isExec('$p/watch_together'), isTrue, reason: 'the old name, a launcher'); expect(_isExec('$p/update.sh'), isTrue); expect(await Link('$p/lib/libapp.so').target(), 'libapp.so.1'); expect(await File('$p/lib/libapp.so').readAsString(), 'lib-v2'); @@ -310,7 +326,7 @@ void main() { manifestUrl: server.manifestUrl, installRoot: root); expect(rc, 0); - expect(await File('${root.path}/watch_together').readAsString(), + expect(await File('${root.path}/starsync').readAsString(), 'binary-v2'); }, skip: _posixOnly); @@ -358,6 +374,7 @@ void main() { expect(await File('$p/lib/libgone.so').readAsString(), 'lib-v1'); expect(await File('$p/data/old.cfg').readAsString(), 'old-config'); expect(File('$p/update.sh').existsSync(), isFalse); + expect(File('$p/starsync').existsSync(), isFalse); expect(_leftovers(root), isEmpty); } @@ -367,6 +384,7 @@ void main() { 'aside:data', 'in:data', 'aside:lib', + 'in:starsync', 'in:update.sh', 'aside:watch_together', 'in:watch_together', @@ -423,12 +441,66 @@ void main() { 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 file-locking semantics (no Windows runner yet). test('windows-style swap applies and parks old files', () async { final root = await _fakeRoot(work, windows: true); await applyUpdate(await archive(windows: true), root, windows: true); 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/flutter_windows.dll').readAsString(), 'dll-v2'); 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/flutter_windows.dll').readAsString(), 'dll-v1'); 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(_leftovers(root), isEmpty); }); diff --git a/packages/server/bin/server.dart b/packages/server/bin/server.dart index 6a519fa..9916892 100644 --- a/packages/server/bin/server.dart +++ b/packages/server/bin/server.dart @@ -8,7 +8,8 @@ import 'package:sync_relay_core/core.dart'; import 'package:sync_relay_server/server.dart'; /// 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 main(List args) async { final parser = ArgParser() ..addOption('port', abbr: 'p', defaultsTo: '8080', help: 'TCP port to listen on (all IPv4 interfaces).') @@ -50,12 +51,12 @@ Future main(List args) async { } if (parsed['help'] as bool) { // 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; } if (parsed['upgrade'] as bool || parsed['check-update'] as bool) { exitCode = await runUpgrade( - program: 'watch-together-server', + program: 'starsync-server', checkOnly: !(parsed['upgrade'] as bool), restartNote: (v) => 'updated to $v — restart the server to run $v.'); return; @@ -67,7 +68,7 @@ Future main(List args) async { (configPath != null ? '${File(configPath).absolute.parent.path}${Platform.pathSeparator}logs' : serverDataDir('logs')); final files = startLogFiles(logDir, prefix: 'server'); 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'); AppConfig config; try { diff --git a/tool/dev_release.sh b/tool/dev_release.sh index 4359274..2512325 100755 --- a/tool/dev_release.sh +++ b/tool/dev_release.sh @@ -55,7 +55,7 @@ export VERSION="$NEXT-dev.$N" echo "building $VERSION (last published: ${LAST:-none})" "$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; } NOTES="$(git -C "$ROOT" log -1 --format=%s)" @@ -90,5 +90,5 @@ cat <-linux-x64.tar.gz containing one -# top-level folder with the executable, lib/ and data/ as built, plus -# update.sh (runs watch-together-server --upgrade) and watch-together-server -# (the dedicated server, `dart compile exe` of packages/server/bin/server.dart, -# built every run, also with --skip-build; both binaries are stamped with -# APP_VERSION from the app's pubspec). The updater strips -# that top-level folder — keep the layout in sync with packages/core/test/updater_test.dart. +# Output: dist/starsync--linux-x64.tar.gz containing one +# top-level folder starsync-/ with the executable `starsync`, lib/ +# and data/ as built, plus update.sh (runs starsync-server --upgrade) and +# starsync-server (the dedicated server, `dart compile exe` of +# packages/server/bin/server.dart, built every run, also with --skip-build; +# both binaries are stamped with APP_VERSION from the app's pubspec), and +# 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). # 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, @@ -28,7 +31,7 @@ done ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" APP="$ROOT/apps/desktop" -NAME="watch-together" +NAME="starsync" ARCH="x64" # VERSION in the environment overrides it (tool/dev_release.sh). @@ -56,7 +59,9 @@ TARBALL="$DIST/$FOLDER-linux-$ARCH.tar.gz" rm -rf "$STAGE" mkdir -p "$STAGE/$FOLDER" "$DIST" 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. DART="dart" if FLUTTER="$(command -v flutter)"; then @@ -64,7 +69,7 @@ if FLUTTER="$(command -v flutter)"; then [[ -x "$CANDIDATE" ]] && DART="$CANDIDATE" fi (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" rm -rf "$STAGE" ls -la "$TARBALL" @@ -119,7 +124,7 @@ payload = { 'accent_color': 5793266, 'components': [ {'type': 10, 'content': - f'# 📦 watch-together `{ver}`\n' + f'# 📦 Starsync `{ver}`\n' f'-# linux · {arch} · release · {size_mb:.1f} MB'}, {'type': 10, 'content': f'`{commit[:140]}`'}, {'type': 10, 'content': block[:1500]}, diff --git a/tool/package_windows.ps1 b/tool/package_windows.ps1 index c05ef56..094b6a7 100644 --- a/tool/package_windows.ps1 +++ b/tool/package_windows.ps1 @@ -1,19 +1,21 @@ # Package the Windows release bundle into a versioned zip. # Usage: powershell -File tool/package_windows.ps1 [-SkipBuild] -# Output: dist/watch-together--windows-x64.zip containing one -# top-level folder with watch_together.exe, its DLLs and data/ as built, -# plus update.bat (runs watch-together-server.exe --upgrade) and -# watch-together-server.exe (dedicated server, `dart compile exe` of -# packages/server/bin/server.dart; both binaries stamped with APP_VERSION). -# Same layout as tool/package_linux.sh; the updater strips the top-level -# folder. +# Output: dist/starsync--windows-x64.zip containing one +# top-level folder starsync-/ with starsync.exe, its DLLs and +# data/ as built, plus update.bat (runs starsync-server.exe --upgrade) and +# starsync-server.exe (dedicated server, `dart compile exe` of +# packages/server/bin/server.dart; both binaries stamped with APP_VERSION), +# and copies of both under the names up to 1.2.x (watch_together.exe, +# 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). param([switch]$SkipBuild) $ErrorActionPreference = 'Stop' $Root = Split-Path -Parent $PSScriptRoot $App = Join-Path $Root 'apps/desktop' -$Name = 'watch-together' +$Name = 'starsync' $Arch = 'x64' $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" -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' $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 Copy-Item -Recurse "$Bundle/*" "$Stage/$Folder/" 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') try { 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 } +Copy-Item "$Stage/$Folder/starsync-server.exe" "$Stage/$Folder/watch-together-server.exe" if (Test-Path $Zip) { Remove-Item -Force $Zip } # bsdtar writes forward-slash entry names (Compress-Archive on 5.1 doesn't). diff --git a/tool/release.sh b/tool/release.sh index 7507bc0..8e70830 100755 --- a/tool/release.sh +++ b/tool/release.sh @@ -55,11 +55,11 @@ PROTO="$(sed -n 's/.*protoVersion = \([0-9]*\).*/\1/p' "$ROOT/packages/core/lib/ if [[ -n "$LATEST" && "$LATEST" != "v$VER" ]]; then NOTES="$(git -C "$ROOT" log -n 20 "$LATEST..HEAD" --format='- %s')" else - NOTES="Watch together $VER." + NOTES="Starsync $VER." fi export MANIFEST_FILE="$ROOT/updates/manifest.json" MANIFEST_VER="$VER" \ 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' import json, os path = os.environ['MANIFEST_FILE'] diff --git a/tool/release_publish.dart b/tool/release_publish.dart index 3b3a6f8..57eac2d 100644 --- a/tool/release_publish.dart +++ b/tool/release_publish.dart @@ -45,7 +45,7 @@ Future main(List args) async { final prev = _git(root, ['describe', '--tags', '--abbrev=0', '$tag^'], allowFail: true); final notes = prev.isEmpty - ? 'Watch together $ver.' + ? 'Starsync $ver.' : _git(root, [ 'log', '$prev..$tag', @@ -63,7 +63,7 @@ Future main(List args) async { rel = await c.post('$api/releases', { 'tag_name': tag, 'name': tag, - 'body': 'Watch together $ver\n\n$notes', + 'body': 'Starsync $ver\n\n$notes', 'draft': false, 'prerelease': false, });