Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81b9d05de6 | ||
|
|
f9b79aa47c | ||
|
|
e16cf50f2c | ||
|
|
8aa1186d29 | ||
|
|
49fee1ca19 | ||
|
|
c91d57633f | ||
|
|
22ce40a488 | ||
|
|
07276ce020 | ||
|
|
4c8e40769a | ||
|
|
211ba65e5e | ||
|
|
fe4749ec7f | ||
|
|
51af830160 | ||
|
|
d9d7881747 |
No files matched your search
@@ -28,6 +28,9 @@ abstract final class MinimalTimelineSpec {
|
|||||||
/// What is left of it while they are hidden.
|
/// What is left of it while they are hidden.
|
||||||
static const lineSize = 3.0;
|
static const lineSize = 3.0;
|
||||||
|
|
||||||
|
/// The played part's accent tint.
|
||||||
|
static const played = 0.4;
|
||||||
|
|
||||||
/// The position line.
|
/// The position line.
|
||||||
static const lineWidth = 2.0;
|
static const lineWidth = 2.0;
|
||||||
static const fontSize = 15.0;
|
static const fontSize = 15.0;
|
||||||
@@ -467,13 +470,10 @@ class MinimalTimelinePainter extends CustomPainter {
|
|||||||
final w = size.width, h = size.height;
|
final w = size.width, h = size.height;
|
||||||
final x = lineX(w);
|
final x = lineX(w);
|
||||||
final band = Offset.zero & size;
|
final band = Offset.zero & size;
|
||||||
// The band, darker as it grows; hidden, a faint track.
|
// The same colours shown and hidden: hidden, it is the band shrunk to
|
||||||
canvas.drawRect(band, Paint()..color = Color.lerp(const Color(0x40FFFFFF), MinimalStyle.band, visibility)!);
|
// a line, so growing and shrinking never switches colour.
|
||||||
// Played: an accent tint, solid once the band is a line.
|
canvas.drawRect(band, Paint()..color = MinimalStyle.band);
|
||||||
canvas.drawRect(
|
canvas.drawRect(Rect.fromLTRB(0, 0, x, h), Paint()..color = accent.withValues(alpha: MinimalTimelineSpec.played));
|
||||||
Rect.fromLTRB(0, 0, x, h),
|
|
||||||
Paint()..color = accent.withValues(alpha: 0.28 + 0.72 * (1 - visibility)),
|
|
||||||
);
|
|
||||||
_stripes(canvas, w, h);
|
_stripes(canvas, w, h);
|
||||||
_chapters(canvas, w, h);
|
_chapters(canvas, w, h);
|
||||||
if (visibility > 0) {
|
if (visibility > 0) {
|
||||||
@@ -500,7 +500,8 @@ class MinimalTimelinePainter extends CustomPainter {
|
|||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.clipRect(Rect.fromLTRB(ax, 0, w, h));
|
canvas.clipRect(Rect.fromLTRB(ax, 0, w, h));
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
..color = Colors.white.withValues(alpha: 0.10 + 0.08 * (1 - visibility))
|
// Stronger on the line, where only a few pixels of each show.
|
||||||
|
..color = Colors.white.withValues(alpha: 0.12 + 0.22 * (1 - visibility))
|
||||||
..strokeWidth = 1.5 * scale;
|
..strokeWidth = 1.5 * scale;
|
||||||
for (var c = (ax / period).floorToDouble() * period - h; c <= w; c += period) {
|
for (var c = (ax / period).floorToDouble() * period - h; c <= w; c += period) {
|
||||||
canvas.drawLine(Offset(c, h), Offset(c + h, 0), paint);
|
canvas.drawLine(Offset(c, h), Offset(c + h, 0), paint);
|
||||||
@@ -509,7 +510,7 @@ class MinimalTimelinePainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _chapters(Canvas canvas, double w, double h) {
|
void _chapters(Canvas canvas, double w, double h) {
|
||||||
final cut = Paint()..color = Colors.black.withValues(alpha: 0.55);
|
final cut = Paint()..color = Colors.black.withValues(alpha: 0.3);
|
||||||
for (final f in chapters) {
|
for (final f in chapters) {
|
||||||
if (f <= 0) continue;
|
if (f <= 0) continue;
|
||||||
final cx = w * f;
|
final cx = w * f;
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ import 'ports/player_host.dart';
|
|||||||
// in a session player with a video output through the media pipeline,
|
// in a session player with a video output through the media pipeline,
|
||||||
// play a second and close the normal way ([shutDownApp]). Progress goes to
|
// play a second and close the normal way ([shutDownApp]). Progress goes to
|
||||||
// `STARSYNC_SMOKE_OUT` (one line per step: `start`, `opened <ms> render=<mode>`,
|
// `STARSYNC_SMOKE_OUT` (one line per step: `start`, `opened <ms> render=<mode>`,
|
||||||
// `closing`, or `skip <why>` / `fail <why>`), so the script can tell a
|
// `closing`, `quitting`, or `skip <why>` / `fail <why>`), so the script can
|
||||||
// runner that cannot show a window (skip) from a close that hangs or
|
// tell a runner that cannot show a window (skip) from a close that hangs or
|
||||||
// leaves a process behind (fail).
|
// leaves a process behind, or a process that ends on its own (fail). It
|
||||||
|
// stays up [smokeUptime] first: a start that dies by itself (the 1.3.0
|
||||||
|
// Windows exit timer) ends before `closing`.
|
||||||
|
|
||||||
|
/// How long the smoke run keeps the app up before closing it.
|
||||||
|
const smokeUptime = Duration(seconds: 10);
|
||||||
|
|
||||||
/// The smoke run's media, or null for a normal start.
|
/// The smoke run's media, or null for a normal start.
|
||||||
String? get smokeMedia {
|
String? get smokeMedia {
|
||||||
@@ -65,6 +70,9 @@ Future<void> runSmoke(AppGraph graph, String media) async {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
step('fail $e');
|
step('fail $e');
|
||||||
}
|
}
|
||||||
|
final wait = smokeUptime - sw.elapsed;
|
||||||
|
if (wait > Duration.zero) await Future.delayed(wait);
|
||||||
step('closing');
|
step('closing');
|
||||||
await shutDownApp();
|
// Reached only through the app's own close (not a kill on the way).
|
||||||
|
await shutDownApp(beforeQuit: () async => step('quitting'));
|
||||||
}
|
}
|
||||||
@@ -517,6 +517,9 @@ class SessionCoordinator {
|
|||||||
final epoch = connection.invalidate();
|
final epoch = connection.invalidate();
|
||||||
await _teardown(silent: true);
|
await _teardown(silent: true);
|
||||||
if (!connection.isCurrent(epoch)) throw const ConnectCancelled();
|
if (!connection.isCurrent(epoch)) throw const ConnectCancelled();
|
||||||
|
// The last server's state is not this one's: kept, its media opened the
|
||||||
|
// player (full, on a friend's server) over an empty new server.
|
||||||
|
serverState.value = null;
|
||||||
connection.begin(params);
|
connection.begin(params);
|
||||||
_wsHost = params.host;
|
_wsHost = params.host;
|
||||||
_wsPort = params.port;
|
_wsPort = params.port;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import 'package:media_kit_video/media_kit_video.dart';
|
|||||||
import 'package:signals/signals_flutter.dart';
|
import 'package:signals/signals_flutter.dart';
|
||||||
|
|
||||||
import '../controls/controls_probe.dart';
|
import '../controls/controls_probe.dart';
|
||||||
|
import '../controls/minimal/style.dart';
|
||||||
|
import '../controls/model/controls_layout.dart';
|
||||||
import '../controls/model/controls_model.dart';
|
import '../controls/model/controls_model.dart';
|
||||||
import '../controls/session_controls.dart';
|
import '../controls/session_controls.dart';
|
||||||
import '../controls/volume.dart';
|
import '../controls/volume.dart';
|
||||||
@@ -291,7 +293,9 @@ class _SessionPlayerState extends State<SessionPlayer> {
|
|||||||
_videoGen = gen;
|
_videoGen = gen;
|
||||||
_videoKey = GlobalKey<VideoState>();
|
_videoKey = GlobalKey<VideoState>();
|
||||||
}
|
}
|
||||||
final VideoControlsBuilder? controls = _full ? _sessionControls : null;
|
// Never null for the mini: media_kit keeps the last controls
|
||||||
|
// when given null, so the full player's would stay under it.
|
||||||
|
final VideoControlsBuilder controls = _full ? _sessionControls : _noControls;
|
||||||
final video = widget.videoBuilder?.call(context, controls) ?? _video(controls);
|
final video = widget.videoBuilder?.call(context, controls) ?? _video(controls);
|
||||||
// The wheel anywhere on the full player steps the volume
|
// The wheel anywhere on the full player steps the volume
|
||||||
// (lists, menus and popovers under the mouse scroll instead).
|
// (lists, menus and popovers under the mouse scroll instead).
|
||||||
@@ -568,11 +572,13 @@ class _SessionPlayerState extends State<SessionPlayer> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Mini: title, expand and close on hover; ready, time and a seekable
|
/// Mini: title, expand and close on hover; the status, play (the full
|
||||||
/// bar at the bottom. A thin progress line stays while not hovered.
|
/// player's play button: ready in a server) and the time, and a bar that
|
||||||
|
/// seeks. A thin progress line stays while not hovered.
|
||||||
Widget _miniOverlay() {
|
Widget _miniOverlay() {
|
||||||
final coord = _coord;
|
final coord = _coord;
|
||||||
final playback = coord.playback;
|
final playback = coord.playback;
|
||||||
|
const scale = 0.8;
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (_) => setState(() => _miniHover = true),
|
onEnter: (_) => setState(() => _miniHover = true),
|
||||||
onExit: (_) => setState(() => _miniHover = false),
|
onExit: (_) => setState(() => _miniHover = false),
|
||||||
@@ -589,55 +595,73 @@ class _SessionPlayerState extends State<SessionPlayer> {
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [Colors.black54, Colors.transparent, Colors.black87],
|
stops: [0, 0.35, 0.55, 1],
|
||||||
|
colors: [Color(0x99000000), Color(0x00000000), Color(0x00000000), Color(0xCC000000)],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 6, 6, 0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 10),
|
const Expanded(
|
||||||
const Expanded(child: NowPlayingTitle(cover: false, compact: true)),
|
child: Padding(
|
||||||
IconButton(
|
padding: EdgeInsets.only(top: 4),
|
||||||
focusNode: _noFocus,
|
child: NowPlayingTitle(cover: false, compact: true),
|
||||||
onPressed: widget.onExpand,
|
),
|
||||||
|
),
|
||||||
|
MinimalButton(
|
||||||
|
icon: Icons.open_in_full_rounded,
|
||||||
tooltip: 'Expand',
|
tooltip: 'Expand',
|
||||||
icon: const Icon(Icons.open_in_full, color: Colors.white, size: 18),
|
scale: scale,
|
||||||
),
|
onPressed: widget.onExpand),
|
||||||
IconButton(
|
MinimalButton(
|
||||||
focusNode: _noFocus,
|
icon: Icons.close_rounded,
|
||||||
onPressed: widget.onClose,
|
|
||||||
tooltip: 'Sit out (stay in the server)',
|
tooltip: 'Sit out (stay in the server)',
|
||||||
icon: const Icon(Icons.close, color: Colors.white, size: 18),
|
scale: scale,
|
||||||
),
|
onPressed: widget.onClose),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
SignalBuilder(builder: (context) {
|
SignalBuilder(builder: (context) {
|
||||||
final ready = coord.selfMember.value?.ready ?? false;
|
final snap = _model.snapshot.value;
|
||||||
final pos = playback.position.value, dur = playback.duration.value;
|
final pos = playback.position.value, dur = playback.duration.value;
|
||||||
return Row(
|
final status = playerStatusNow(coord)?.short;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 0, 12, 2),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
if (status != null && status.isNotEmpty)
|
||||||
focusNode: _noFocus,
|
Padding(
|
||||||
tooltip: ready ? 'Ready (click: not ready)' : 'Not ready (click: ready)',
|
padding: const EdgeInsets.fromLTRB(4, 0, 0, 2),
|
||||||
onPressed: coord.toggleReady,
|
child: Text(status,
|
||||||
icon: Icon(
|
|
||||||
ready ? Icons.check_circle : Icons.radio_button_unchecked,
|
|
||||||
color: ready ? Colors.greenAccent : Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text('${formatClock(pos)} / ${formatClock(dur)}',
|
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 12)),
|
|
||||||
const Spacer(),
|
|
||||||
Flexible(
|
|
||||||
child: Text(playerStatusNow(_coord)?.short ?? '',
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(color: Colors.amber, fontSize: 12)),
|
style: const TextStyle(color: Color(0xFFFFC857), fontSize: 12)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
Row(
|
||||||
|
children: [
|
||||||
|
MinimalButton(
|
||||||
|
icon: controlIcon(BarControl.playPause, snap),
|
||||||
|
tooltip: controlTooltip(BarControl.playPause, snap),
|
||||||
|
accent: true,
|
||||||
|
scale: scale,
|
||||||
|
onPressed: _model.togglePlay,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('${formatClock(pos)} / ${formatClock(dur)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: MinimalStyle.dim,
|
||||||
|
fontSize: 12,
|
||||||
|
fontFeatures: [FontFeature.tabularFigures()])),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
_MiniSeekBar(onSeek: playback.userSeek),
|
_MiniSeekBar(onSeek: playback.userSeek),
|
||||||
@@ -654,6 +678,9 @@ class _SessionPlayerState extends State<SessionPlayer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The mini player's empty controls slot (see [SessionPlayer]'s video).
|
||||||
|
Widget _noControls(VideoState state) => const SizedBox.shrink();
|
||||||
|
|
||||||
/// A floating pill's visibility: only while the controls are up (the
|
/// A floating pill's visibility: only while the controls are up (the
|
||||||
/// mouse moved) or the mouse is on it. With [announce] it is also up for
|
/// mouse moved) or the mouse is on it. With [announce] it is also up for
|
||||||
/// [firstShown] whenever [kind] changes (a new offer: Skip OP, Skip 1:30).
|
/// [firstShown] whenever [kind] changes (a new offer: Skip OP, Skip 1:30).
|
||||||
@@ -768,7 +795,9 @@ const _shaderKeys = [
|
|||||||
LogicalKeyboardKey.digit6,
|
LogicalKeyboardKey.digit6,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Mini player progress: a 2 px line, or (hovered) a 4 px bar that seeks.
|
/// Mini player progress across the bottom edge: a 3 px line, or (hovered)
|
||||||
|
/// a 4 px bar with its track and the cached part that seeks (taller to
|
||||||
|
/// hit). Plain rectangles: the player's rounded clip shapes the ends.
|
||||||
class _MiniSeekBar extends StatelessWidget {
|
class _MiniSeekBar extends StatelessWidget {
|
||||||
final bool thin;
|
final bool thin;
|
||||||
final ValueChanged<Duration>? onSeek;
|
final ValueChanged<Duration>? onSeek;
|
||||||
@@ -777,26 +806,59 @@ class _MiniSeekBar extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final playback = AppScope.of(context).coordinator.playback;
|
final playback = AppScope.of(context).coordinator.playback;
|
||||||
|
final accent = Theme.of(context).colorScheme.primary;
|
||||||
return SignalBuilder(builder: (context) {
|
return SignalBuilder(builder: (context) {
|
||||||
final d = playback.duration.value;
|
final d = playback.duration.value;
|
||||||
final p = playback.position.value;
|
double f(Duration x) => d.inMilliseconds == 0 ? 0.0 : (x.inMilliseconds / d.inMilliseconds).clamp(0.0, 1.0);
|
||||||
final v = d.inMilliseconds == 0 ? 0.0 : (p.inMilliseconds / d.inMilliseconds).clamp(0.0, 1.0);
|
final bar = SizedBox(
|
||||||
final bar = LinearProgressIndicator(
|
height: thin ? 3 : 4,
|
||||||
value: v,
|
width: double.infinity,
|
||||||
minHeight: thin ? 2 : 4,
|
child: CustomPaint(
|
||||||
backgroundColor: thin ? Colors.transparent : Colors.white24,
|
painter: _MiniBarPainter(
|
||||||
|
played: f(playback.position.value),
|
||||||
|
cached: thin ? null : f(playback.buffered.value),
|
||||||
|
accent: accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (thin || onSeek == null || d == Duration.zero) return bar;
|
if (thin || onSeek == null || d == Duration.zero) return bar;
|
||||||
void seekAt(double x, double width) => onSeek!(d * (x / width).clamp(0.0, 1.0));
|
void seekAt(double x, double width) => onSeek!(d * (x / width).clamp(0.0, 1.0));
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, box) => GestureDetector(
|
builder: (context, box) => MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTapDown: (e) => seekAt(e.localPosition.dx, box.maxWidth),
|
onTapDown: (e) => seekAt(e.localPosition.dx, box.maxWidth),
|
||||||
onHorizontalDragEnd: null,
|
|
||||||
onHorizontalDragUpdate: (e) => seekAt(e.localPosition.dx, box.maxWidth),
|
onHorizontalDragUpdate: (e) => seekAt(e.localPosition.dx, box.maxWidth),
|
||||||
child: Padding(padding: const EdgeInsets.symmetric(vertical: 6), child: bar),
|
child: Padding(padding: const EdgeInsets.only(top: 8), child: bar),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MiniBarPainter extends CustomPainter {
|
||||||
|
final double played;
|
||||||
|
|
||||||
|
/// End of the cache ahead (a track is drawn only with it).
|
||||||
|
final double? cached;
|
||||||
|
final Color accent;
|
||||||
|
const _MiniBarPainter({required this.played, required this.cached, required this.accent});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final w = size.width, h = size.height;
|
||||||
|
final c = cached;
|
||||||
|
if (c != null) {
|
||||||
|
canvas.drawRect(Offset.zero & size, Paint()..color = Colors.white.withValues(alpha: 0.18));
|
||||||
|
if (c > played) {
|
||||||
|
canvas.drawRect(Rect.fromLTRB(w * played, 0, w * c, h), Paint()..color = Colors.white.withValues(alpha: 0.3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.drawRect(Rect.fromLTRB(0, 0, w * played, h), Paint()..color = accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_MiniBarPainter old) => old.played != played || old.cached != cached || old.accent != accent;
|
||||||
|
}
|
||||||
@@ -865,14 +865,14 @@ packages:
|
|||||||
path: "../../packages/core"
|
path: "../../packages/core"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
sync_relay_server:
|
sync_relay_server:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "../../packages/server"
|
path: "../../packages/server"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: starsync
|
name: starsync
|
||||||
description: Starsync desktop app (repo sync_relay_project). One binary.
|
description: Starsync desktop app (repo sync_relay_project). One binary.
|
||||||
publish_to: none
|
publish_to: none
|
||||||
version: 1.3.0
|
version: 1.3.2
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.13.0-0 <4.0.0'
|
sdk: '>=3.13.0-0 <4.0.0'
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:hive_ce/hive.dart';
|
import 'package:hive_ce/hive.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
import 'package:signals/signals.dart' show effect;
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:sync_relay_core/testing.dart';
|
import 'package:sync_relay_core/testing.dart';
|
||||||
import 'package:sync_relay_server/server.dart';
|
import 'package:sync_relay_server/server.dart';
|
||||||
@@ -443,6 +444,39 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('moving to another server never shows the last one\'s media', timeout: const Timeout(Duration(minutes: 1)),
|
||||||
|
() async {
|
||||||
|
final home = SyncServer(const AppConfig(port: 0, secret: 'pw', name: 'home'));
|
||||||
|
final empty = SyncServer(const AppConfig(port: 0, secret: 'pw', name: 'empty'));
|
||||||
|
await home.start();
|
||||||
|
await empty.start();
|
||||||
|
final origin = await _origin(await _media());
|
||||||
|
try {
|
||||||
|
final alice = await member(home.boundPort, 'alice');
|
||||||
|
expect(await alice.swapPicked(LinkMedia('http://127.0.0.1:${origin.port}/ep.mkv', anyway: true)), isNull);
|
||||||
|
await _poll(() => alice.serverState.peek()?.media != null, 'media up at home');
|
||||||
|
// The shell opens the player on any state with media as soon as the
|
||||||
|
// app is in a server again: nothing from the last one may be left
|
||||||
|
// while the new one is dialled.
|
||||||
|
final seen = <String>[];
|
||||||
|
final stop = effect(() {
|
||||||
|
if (alice.connection.state.value is ConnConnecting) {
|
||||||
|
seen.add(alice.serverState.peek()?.media?['name'] as String? ?? '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await alice.connect(ConnectParams(host: '127.0.0.1', port: empty.boundPort, secret: 'pw', name: 'alice'));
|
||||||
|
await _poll(() => alice.serverState.peek() != null, 'the new server\'s state');
|
||||||
|
stop();
|
||||||
|
expect(seen, isNotEmpty);
|
||||||
|
expect(seen.where((n) => n.isNotEmpty), isEmpty, reason: 'no media from the last server');
|
||||||
|
expect(alice.serverState.peek()?.media, isNull);
|
||||||
|
} finally {
|
||||||
|
await home.stop();
|
||||||
|
await empty.stop();
|
||||||
|
await origin.close(force: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('cancel during connect setup aborts without joining', timeout: const Timeout(Duration(minutes: 1)), () async {
|
test('cancel during connect setup aborts without joining', timeout: const Timeout(Duration(minutes: 1)), () async {
|
||||||
final server = SyncServer(const AppConfig(port: 0, secret: '', name: 'cancel'));
|
final server = SyncServer(const AppConfig(port: 0, secret: '', name: 'cancel'));
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|||||||
@@ -418,6 +418,25 @@ void main() {
|
|||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('the mini player clears the full player\'s controls (media_kit keeps the last ones on null)',
|
||||||
|
(tester) async {
|
||||||
|
coord.playback.alive.value = true;
|
||||||
|
final given = <PlayerMode, Object?>{};
|
||||||
|
PlayerMode mode() => tester.state<AppShellState>(find.byType(AppShell)).mode;
|
||||||
|
await pumpShell(tester, const Size(1280, 800), video: (context, controls) {
|
||||||
|
given[mode()] = controls;
|
||||||
|
return const ColoredBox(color: Color(0xFF7A3B2E));
|
||||||
|
});
|
||||||
|
online(party);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const ValueKey('tab-settings')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(mode(), PlayerMode.mini);
|
||||||
|
expect(given[PlayerMode.full], isNotNull);
|
||||||
|
expect(given[PlayerMode.mini], isNotNull, reason: 'null would keep the full controls');
|
||||||
|
expect(given[PlayerMode.mini], isNot(same(given[PlayerMode.full])));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('status pill: who is not ready, with the ready count', (tester) async {
|
testWidgets('status pill: who is not ready, with the ready count', (tester) async {
|
||||||
coord.playback.alive.value = true;
|
coord.playback.alive.value = true;
|
||||||
await pumpShell(tester, const Size(1280, 800));
|
await pumpShell(tester, const Size(1280, 800));
|
||||||
|
|||||||
@@ -80,7 +80,11 @@ bool FlutterWindow::OnCreate() {
|
|||||||
|
|
||||||
void FlutterWindow::OnDestroy() {
|
void FlutterWindow::OnDestroy() {
|
||||||
// The engine and the plugins go now; the app released its players first.
|
// The engine and the plugins go now; the app released its players first.
|
||||||
|
// Win32Window::Create runs this too, before there is an engine: only a
|
||||||
|
// real teardown may end the process (else every start dies at 3 s).
|
||||||
|
if (flutter_controller_) {
|
||||||
ExitAfter(3000);
|
ExitAfter(3000);
|
||||||
|
}
|
||||||
window_channel_ = nullptr;
|
window_channel_ = nullptr;
|
||||||
if (flutter_controller_) {
|
if (flutter_controller_) {
|
||||||
flutter_controller_ = nullptr;
|
flutter_controller_ = nullptr;
|
||||||
|
|||||||
+16
-4
@@ -1958,7 +1958,13 @@ panel) and chat messages (Copy message, and a link in it).
|
|||||||
snaps to the nearest corner with the shared curve). A mode change fades
|
snaps to the nearest corner with the shared curve). A mode change fades
|
||||||
the layer out, moves it while invisible and fades it in (with the tabs
|
the layer out, moves it while invisible and fades it in (with the tabs
|
||||||
when the full player comes or goes), so the video is never mounted twice
|
when the full player comes or goes), so the video is never mounted twice
|
||||||
nor remounted. A tab sends the full player to mini; × on the mini sits
|
nor remounted. The mini has no control skin: its `Video` gets an empty
|
||||||
|
controls builder (never null: media_kit keeps the last controls on
|
||||||
|
null, which left the full player's bars under the mini). Hovered it
|
||||||
|
shows the title with expand and ×, the status line, the full player's
|
||||||
|
play button (`controlIcon`/`togglePlay`: ready in a server) with the
|
||||||
|
time, and a 4 px bar (track, cached, played) that seeks; otherwise a
|
||||||
|
3 px played line along the bottom edge. A tab sends the full player to mini; × on the mini sits
|
||||||
out (the player unloads and shows the join-back card, `SitOutCard`, over
|
out (the player unloads and shows the join-back card, `SitOutCard`, over
|
||||||
the still mounted `Video`); × on that card closes the player, the strip
|
the still mounted `Video`); × on that card closes the player, the strip
|
||||||
brings it back, and joining back with it closed shows the mini. Fullscreen
|
brings it back, and joining back with it closed shows the mini. Fullscreen
|
||||||
@@ -2204,7 +2210,7 @@ Player controls (`lib/src/controls/`): one controls model, two skins.
|
|||||||
glass capsules (`MinimalCapsule`, the pills' tint and hairline;
|
glass capsules (`MinimalCapsule`, the pills' tint and hairline;
|
||||||
`MinimalButton` rounded squares, the play button a filled accent
|
`MinimalButton` rounded squares, the play button a filled accent
|
||||||
circle) above `MinimalTimeline`, a 36 px band across the bottom: the
|
circle) above `MinimalTimeline`, a 36 px band across the bottom: the
|
||||||
played part tinted in the theme's primary, the position a white line,
|
played part tinted in the theme's primary (40 %), the position a white line,
|
||||||
the part not downloaded yet (from `buffered`, or the position when
|
the part not downloaded yet (from `buffered`, or the position when
|
||||||
that is behind, to the end) in thin stripes, chapter starts as dots on
|
that is behind, to the end) in thin stripes, chapter starts as dots on
|
||||||
the top edge with a dark cut through the band (a press within reach of
|
the top edge with a dark cut through the band (a press within reach of
|
||||||
@@ -2214,7 +2220,11 @@ Player controls (`lib/src/controls/`): one controls model, two skins.
|
|||||||
not reach the end). Hover: a faint line and a glass tip with the time
|
not reach the end). Hover: a faint line and a glass tip with the time
|
||||||
(the chapter title above it). A press seeks, every drag move seeks,
|
(the chapter title above it). A press seeks, every drag move seeks,
|
||||||
after release the line stays at the target until the position catches
|
after release the line stays at the target until the position catches
|
||||||
up (at most 2 s). Hidden, it shrinks (120 ms) to a 3 px progress line.
|
up (at most 2 s). Hidden, it shrinks (120 ms) to a 3 px progress line
|
||||||
|
that is the band itself, smaller: the same dark band, the same played
|
||||||
|
tint (40 % of the accent), so growing or shrinking never changes colour;
|
||||||
|
the stripes are stronger on the line (a few pixels of each show) so the
|
||||||
|
part not downloaded still reads; chapter cuts are a light dark line.
|
||||||
We know only the end of mpv's cache, so a dropped back buffer is not
|
We know only the end of mpv's cache, so a dropped back buffer is not
|
||||||
shown.
|
shown.
|
||||||
- **Settings** (`AppSettings`, `model/controls_prefs.dart`, all live):
|
- **Settings** (`AppSettings`, `model/controls_prefs.dart`, all live):
|
||||||
@@ -2283,7 +2293,9 @@ app kills itself (`Process.killPid`, no exit handlers; it only gets there
|
|||||||
if the close did not go through); the runners end the process 15 s after
|
if the close did not go through); the runners end the process 15 s after
|
||||||
the first close request (Linux: a `delete-event` handler connected before
|
the first close request (Linux: a `delete-event` handler connected before
|
||||||
window_manager's; Windows: the first `WM_CLOSE`), and Windows also 3 s
|
window_manager's; Windows: the first `WM_CLOSE`), and Windows also 3 s
|
||||||
after the window is destroyed, while the engine and plugins go.
|
after the window is destroyed, while the engine and plugins go (only
|
||||||
|
with an engine: `Win32Window::Create` runs `OnDestroy` before the window
|
||||||
|
exists, and a timer armed there ended every start at 3 s).
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
|
|||||||
+14
-4
@@ -210,10 +210,12 @@ updater tests, the release build and the close smoke run on that build
|
|||||||
`STARSYNC_SMOKE=<clip>` (a 5 s clip from ffmpeg, else a WAV the script
|
`STARSYNC_SMOKE=<clip>` (a 5 s clip from ffmpeg, else a WAV the script
|
||||||
writes) under its own profile (`STARSYNC_PROFILE=smoke`), opens the clip
|
writes) under its own profile (`STARSYNC_PROFILE=smoke`), opens the clip
|
||||||
in a player with a video output through the media pipeline
|
in a player with a video output through the media pipeline
|
||||||
(`platform/smoke.dart`), plays a second and closes the normal way; the
|
(`platform/smoke.dart`), plays it, stays up 10 s from its start and
|
||||||
steps go to `STARSYNC_SMOKE_OUT` (`start`, `opened <ms> render=<mode>`,
|
closes the normal way; the steps go to `STARSYNC_SMOKE_OUT` (`start`,
|
||||||
`closing`, or `skip` / `fail` with why). It fails when the app does not
|
`opened <ms> render=<mode>`, `closing`, `quitting` just before the
|
||||||
exit within 45 s or leaves a `starsync` process, skips with a warning
|
window closes, or `skip` / `fail` with why). It fails when the app ends
|
||||||
|
by itself before `closing` or never reaches `quitting`, does not exit
|
||||||
|
within 45 s or leaves a `starsync` process, skips with a warning
|
||||||
when the runner cannot bring the app or its video output up, and prints
|
when the runner cannot bring the app or its video output up, and prints
|
||||||
the `smoke:`, `close:` and `mpv: render` lines of the app log.
|
the `smoke:`, `close:` and `mpv: render` lines of the app log.
|
||||||
`tool/smoke_linux.sh` is the same on Linux (needs a display; after
|
`tool/smoke_linux.sh` is the same on Linux (needs a display; after
|
||||||
@@ -302,6 +304,14 @@ in the same file; a dedicated server prints its folder at start
|
|||||||
(`--log-dir`). Users send theirs with Settings → About → "Copy
|
(`--log-dir`). Users send theirs with Settings → About → "Copy
|
||||||
diagnostics".
|
diagnostics".
|
||||||
|
|
||||||
|
A Windows friend without a terminal habit: `tool/windows_report.cmd`
|
||||||
|
(one file: a cmd header that runs the PowerShell below it). Dropped into
|
||||||
|
the Starsync folder and double-clicked, it collects the app's logs,
|
||||||
|
Windows' crash events (1000/1001/1026), that install's file list (and
|
||||||
|
any other found on Desktop, Downloads, Documents, AppData, Program
|
||||||
|
Files) and a 20 s test start with its exit code, zips them to the
|
||||||
|
Desktop and copies the zip (Ctrl+V into Discord).
|
||||||
|
|
||||||
Start with the session log when there is one: `session-YYYY-MM-DD.log`
|
Start with the session log when there is one: `session-YYYY-MM-DD.log`
|
||||||
in the server's folder (the hosting app's logs folder, or the dedicated
|
in the server's folder (the hosting app's logs folder, or the dedicated
|
||||||
server's `--log-dir`), every member's lines and the server's merged in
|
server's `--log-dir`), every member's lines and the server's merged in
|
||||||
|
|||||||
+17
-9
@@ -52,6 +52,12 @@ Problems are numbered; delete a line when its fix ships.
|
|||||||
the `close:` log lines.
|
the `close:` log lines.
|
||||||
- P33 No volume readout on wheel: likely fixed by the controls rework
|
- P33 No volume readout on wheel: likely fixed by the controls rework
|
||||||
(1.2.1-dev.1; a test covers both skins); confirm in real use.
|
(1.2.1-dev.1; a test covers both skins); confirm in real use.
|
||||||
|
- P35 System notifications in a burst hit KDE Plasma's limit
|
||||||
|
(`ExcessNotificationGeneration`, 3 refused within 3 s after a swap while
|
||||||
|
the window was in the background): throttle or merge them app side.
|
||||||
|
- P36 After a reconnect the app re-dials the warm relay of an Up next
|
||||||
|
item that was already played ("relay channel refused: not the source of
|
||||||
|
Up next item q8"): harmless, the feeder should end with its item.
|
||||||
|
|
||||||
**Restructure** (in order; reviewed against the code). Truth lives in
|
**Restructure** (in order; reviewed against the code). Truth lives in
|
||||||
three places: the server (members, presence, gate, transport, queue incl.
|
three places: the server (members, presence, gate, transport, queue incl.
|
||||||
@@ -59,9 +65,8 @@ the automatic continuation and countdown, relay state), the device
|
|||||||
(client id, library and folders, match corrections, watch history,
|
(client id, library and folders, match corrections, watch history,
|
||||||
settings, notification prefs, what this app is sourcing) and the
|
settings, notification prefs, what this app is sourcing) and the
|
||||||
integrations (Jellyfin played/resume, AniList list).
|
integrations (Jellyfin played/resume, AniList list).
|
||||||
7. Release 1.3.0: publish the manifest (protocol 16 makes it a required
|
Released as 1.3.0. Left: check that a 1.2.x app gets a clear "update
|
||||||
update for 1.2.x) and update the VPS dedicated server in the same
|
the app" message and updates (Windows: the first real use, backlog 5).
|
||||||
window; check a 1.2.x app gets a clear "update the app" message.
|
|
||||||
Composition root (in place, ARCHITECTURE "Composition root"): the
|
Composition root (in place, ARCHITECTURE "Composition root"): the
|
||||||
remaining `.instance` singletons move into `AppGraph` as the steps touch
|
remaining `.instance` singletons move into `AppGraph` as the steps touch
|
||||||
them; `singletons_test` holds the count (today: AppSettings 49,
|
them; `singletons_test` holds the count (today: AppSettings 49,
|
||||||
@@ -246,6 +251,10 @@ core and app. Still open on the wire, frozen at the 1.3.0 release:
|
|||||||
3. **AniList account**: sign in (OAuth via a localhost callback), "My list"
|
3. **AniList account**: sign in (OAuth via a localhost callback), "My list"
|
||||||
filter on the Schedule from the user's AniList lists. This is the
|
filter on the Schedule from the user's AniList lists. This is the
|
||||||
Schedule's personal tracking; AnimeFlow has no per-user follow.
|
Schedule's personal tracking; AnimeFlow has no per-user follow.
|
||||||
|
**AniList browser** (wanted next once 1.3.0 holds up): view shows and
|
||||||
|
browse older ones (search, seasons, details), and request one from
|
||||||
|
there on AnimeFlow; later on Sonarr too, which is what a public
|
||||||
|
release of the app would use.
|
||||||
|
|
||||||
4. **Ops**: Check shaders on Windows; run the relay e2e "the source drops
|
4. **Ops**: Check shaders on Windows; run the relay e2e "the source drops
|
||||||
uncleanly and comes back" a few times under the full suite (its
|
uncleanly and comes back" a few times under the full suite (its
|
||||||
@@ -292,12 +301,11 @@ core and app. Still open on the wire, frozen at the 1.3.0 release:
|
|||||||
8. **Jellyfin** (epic, below).
|
8. **Jellyfin** (epic, below).
|
||||||
|
|
||||||
9. **Smaller items**
|
9. **Smaller items**
|
||||||
- Video output robustness: if media_kit's hardware path regresses to
|
- The video freeze after GPU idle: next suspect is media_kit's context
|
||||||
software (see ARCHITECTURE, Media: render mode), vendor a
|
switching on the raster thread with NVIDIA.
|
||||||
media_kit_video fix that builds the EGL display from GDK's native
|
- Timelines: draw mpv's real cached ranges (`demuxer-cache-state`
|
||||||
display. The video freeze after GPU idle is not explained by this;
|
`seekable-ranges`, read through media_kit's `observeProperty`), not
|
||||||
next suspect is media_kit's context switching on the raster thread
|
only the end of the cache ahead, like uosc.
|
||||||
with NVIDIA.
|
|
||||||
- Link conveniences: a link swap in session updates the remembered media;
|
- Link conveniences: a link swap in session updates the remembered media;
|
||||||
show the open link with an open-in-browser button.
|
show the open link with an open-in-browser button.
|
||||||
- Time UI: remaining time, scrubber preview.
|
- Time UI: remaining time, scrubber preview.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: sync_relay_core
|
name: sync_relay_core
|
||||||
description: Pure-Dart protocol, sync engine, media-source abstractions. No Flutter.
|
description: Pure-Dart protocol, sync engine, media-source abstractions. No Flutter.
|
||||||
version: 1.3.0
|
version: 1.3.2
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.0.0
|
sdk: ^3.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: sync_relay_server
|
name: sync_relay_server
|
||||||
description: Pure-Dart sync + media server. Embeddable, no Flutter.
|
description: Pure-Dart sync + media server. Embeddable, no Flutter.
|
||||||
publish_to: none
|
publish_to: none
|
||||||
version: 1.3.0
|
version: 1.3.2
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.0.0
|
sdk: ^3.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ void main() {
|
|||||||
final clients = <_Client>[];
|
final clients = <_Client>[];
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
// Short limits so the demo runs in a couple of seconds.
|
// Short limits so the demo runs in a few seconds; silence well above
|
||||||
|
// a busy CI runner's stalls (600 ms marked a reporting client gone).
|
||||||
server = SyncServer(AppConfig(port: 0, secret: 'shh', name: 'test'),
|
server = SyncServer(AppConfig(port: 0, secret: 'shh', name: 'test'),
|
||||||
liveness: const Liveness(silence: Duration(milliseconds: 600)));
|
liveness: const Liveness(silence: Duration(milliseconds: 1500)));
|
||||||
await server.start();
|
await server.start();
|
||||||
proxy = await FlakyProxy.start(server.boundPort);
|
proxy = await FlakyProxy.start(server.boundPort);
|
||||||
});
|
});
|
||||||
@@ -106,7 +107,7 @@ void main() {
|
|||||||
.toJson());
|
.toJson());
|
||||||
c.send(const SwapIntent(media: HelloMedia(name: 'mine.mkv', size: 100, duration: 0, relay: MediaRelay())).toJson());
|
c.send(const SwapIntent(media: HelloMedia(name: 'mine.mkv', size: 100, duration: 0, relay: MediaRelay())).toJson());
|
||||||
await until(() => engine.queue.length == 1 && server.hub.relay != null && member()!.ready, 'ready, queued, relayed');
|
await until(() => engine.queue.length == 1 && server.hub.relay != null && member()!.ready, 'ready, queued, relayed');
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 1200));
|
await Future<void>.delayed(const Duration(milliseconds: 2000));
|
||||||
expect(member()!.presence, isNot(Presence.gone), reason: 'reports through the proxy keep it present');
|
expect(member()!.presence, isNot(Presence.gone), reason: 'reports through the proxy keep it present');
|
||||||
|
|
||||||
proxy.last!.blackhole();
|
proxy.last!.blackhole();
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ grep -qx start "$OUT" 2>/dev/null || { [[ $exited == 1 ]] && { echo "skipped: th
|
|||||||
grep -q '^skip ' "$OUT" && { echo "skipped: $(grep '^skip ' "$OUT")"; exit 0; }
|
grep -q '^skip ' "$OUT" && { echo "skipped: $(grep '^skip ' "$OUT")"; exit 0; }
|
||||||
grep -q '^fail ' "$OUT" && { echo "smoke failed: $(grep '^fail ' "$OUT")" >&2; exit 1; }
|
grep -q '^fail ' "$OUT" && { echo "smoke failed: $(grep '^fail ' "$OUT")" >&2; exit 1; }
|
||||||
grep -q '^opened ' "$OUT" || { echo "the media never opened" >&2; exit 1; }
|
grep -q '^opened ' "$OUT" || { echo "the media never opened" >&2; exit 1; }
|
||||||
|
grep -qx closing "$OUT" || { echo "the app ended on its own before closing" >&2; exit 1; }
|
||||||
|
grep -qx quitting "$OUT" || { echo "the close did not reach its end (killed on the way?)" >&2; exit 1; }
|
||||||
[[ $exited == 1 ]] || { echo "the app did not close within $LIMIT s (P32)" >&2; exit 1; }
|
[[ $exited == 1 ]] || { echo "the app did not close within $LIMIT s (P32)" >&2; exit 1; }
|
||||||
[[ -z "$left" ]] || { echo "a starsync process was left behind (P32)" >&2; exit 1; }
|
[[ -z "$left" ]] || { echo "a starsync process was left behind (P32)" >&2; exit 1; }
|
||||||
echo "smoke ok: opened and closed in $secs s, no process left"
|
echo "smoke ok: opened and closed in $secs s, no process left"
|
||||||
@@ -64,6 +64,8 @@ if (-not ($lines -contains 'start')) {
|
|||||||
if ($lines | Where-Object { $_ -like 'skip *' }) { Write-Warning "skipped: $($lines -like 'skip *')"; exit 0 }
|
if ($lines | Where-Object { $_ -like 'skip *' }) { Write-Warning "skipped: $($lines -like 'skip *')"; exit 0 }
|
||||||
if ($lines | Where-Object { $_ -like 'fail *' }) { Write-Error "smoke failed: $($lines -like 'fail *')"; exit 1 }
|
if ($lines | Where-Object { $_ -like 'fail *' }) { Write-Error "smoke failed: $($lines -like 'fail *')"; exit 1 }
|
||||||
if (-not ($lines | Where-Object { $_ -like 'opened *' })) { Write-Error 'the media never opened'; exit 1 }
|
if (-not ($lines | Where-Object { $_ -like 'opened *' })) { Write-Error 'the media never opened'; exit 1 }
|
||||||
|
if (-not ($lines -contains 'closing')) { Write-Error "the app ended on its own before closing (exit $($p.ExitCode))"; exit 1 }
|
||||||
|
if (-not ($lines -contains 'quitting')) { Write-Error 'the close did not reach its end (killed on the way?)'; exit 1 }
|
||||||
if (-not $exited) { Write-Error "the app did not close within $Limit s (P32)"; exit 1 }
|
if (-not $exited) { Write-Error "the app did not close within $Limit s (P32)"; exit 1 }
|
||||||
if ($left) { Write-Error "a starsync process was left behind (P32)"; exit 1 }
|
if ($left) { Write-Error "a starsync process was left behind (P32)"; exit 1 }
|
||||||
Write-Host "smoke ok: opened and closed in $secs s, no process left"
|
Write-Host "smoke ok: opened and closed in $secs s, no process left"
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
@echo off
|
||||||
|
rem Starsync crash report: put this file in the Starsync folder (next to
|
||||||
|
rem starsync.exe or watch_together.exe) and double-click it. It makes
|
||||||
|
rem starsync-report.zip on the Desktop and copies it: Ctrl+V in Discord.
|
||||||
|
set "STARSYNC_REPORT_DIR=%~dp0"
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$s = Get-Content -LiteralPath '%~f0' -Raw; iex $s.Substring($s.IndexOf('#STAR' + 'SYNC-PS'))"
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
#STARSYNC-PS
|
||||||
|
# Starsync crash report (run windows_report.cmd from the Starsync folder): collects logs, crash events and a test start, zips
|
||||||
|
# them to the Desktop and puts the zip on the clipboard (Ctrl+V in Discord).
|
||||||
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
|
$o = Join-Path $env:TEMP 'starsync-report'
|
||||||
|
Remove-Item -Recurse -Force $o
|
||||||
|
New-Item -ItemType Directory $o | Out-Null
|
||||||
|
$info = Join-Path $o 'info.txt'
|
||||||
|
function Say($s) { Add-Content -Path $info -Value $s }
|
||||||
|
|
||||||
|
Say ('Report ' + (Get-Date -Format s))
|
||||||
|
Say ((Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, OSArchitecture | Format-List | Out-String).Trim())
|
||||||
|
Say ((Get-CimInstance Win32_VideoController | Select-Object Name, DriverVersion | Format-List | Out-String).Trim())
|
||||||
|
|
||||||
|
# The app's logs (new and old data folders), newest first.
|
||||||
|
$logs = Get-ChildItem -Path "$env:APPDATA\*\*\logs\*" -Include 'app-*', 'diagnostics-*', 'session-*', 'server-*' |
|
||||||
|
Sort-Object LastWriteTime -Descending | Select-Object -First 10
|
||||||
|
foreach ($l in $logs) {
|
||||||
|
$dest = Join-Path $o ($l.Directory.Parent.Name + '_' + $l.Name)
|
||||||
|
Copy-Item $l.FullName $dest
|
||||||
|
}
|
||||||
|
Say ''
|
||||||
|
Say 'Data folders:'
|
||||||
|
Get-ChildItem -Path "$env:APPDATA\*\*" -Directory | Where-Object { $_.Name -match 'starsync|watch' } |
|
||||||
|
ForEach-Object { Say (' ' + $_.FullName) }
|
||||||
|
|
||||||
|
# Windows' own crash records for the app.
|
||||||
|
Get-WinEvent -FilterHashtable @{ LogName = 'Application'; Id = 1000, 1001, 1026; StartTime = (Get-Date).AddDays(-3) } |
|
||||||
|
Where-Object { $_.Message -match 'starsync|watch_together' } |
|
||||||
|
Format-List TimeCreated, Id, ProviderName, Message | Out-File (Join-Path $o 'crash-events.txt') -Width 300
|
||||||
|
|
||||||
|
# Every install found: its files, and one test start of starsync.exe.
|
||||||
|
$roots = @([Environment]::GetFolderPath('Desktop'), "$env:USERPROFILE\Downloads", "$env:USERPROFILE\Desktop", $env:LOCALAPPDATA, $env:ProgramFiles, "$env:USERPROFILE\Documents") | Select-Object -Unique
|
||||||
|
$exes = foreach ($r in $roots) { Get-ChildItem -Path $r -Recurse -Depth 3 -Include 'starsync.exe', 'watch_together.exe' -File }
|
||||||
|
# The folder this report was started from comes first.
|
||||||
|
$here = $env:STARSYNC_REPORT_DIR
|
||||||
|
if ($here) { $here = $here.TrimEnd('\') }
|
||||||
|
$dirs = @($here) + ($exes | ForEach-Object { $_.DirectoryName }) |
|
||||||
|
Where-Object { $_ -and ((Test-Path (Join-Path $_ 'starsync.exe')) -or (Test-Path (Join-Path $_ 'watch_together.exe'))) } |
|
||||||
|
Select-Object -Unique
|
||||||
|
foreach ($d in $dirs) {
|
||||||
|
Say ''
|
||||||
|
Say ('Install: ' + $d)
|
||||||
|
Get-ChildItem -Path $d -Recurse -File | ForEach-Object {
|
||||||
|
Say (' ' + $_.FullName.Substring($d.Length) + ' ' + $_.Length + ' ' + $_.LastWriteTime.ToString('s'))
|
||||||
|
}
|
||||||
|
$exe = Join-Path $d 'starsync.exe'
|
||||||
|
if (-not (Test-Path $exe)) { $exe = Join-Path $d 'watch_together.exe' }
|
||||||
|
$p = Start-Process -FilePath $exe -WorkingDirectory $d -PassThru
|
||||||
|
Start-Sleep -Seconds 20
|
||||||
|
if ($p.HasExited) {
|
||||||
|
Say ('Test start of ' + $exe + ': exited, code ' + $p.ExitCode + ' (0x' + ('{0:X8}' -f $p.ExitCode) + ')')
|
||||||
|
} else {
|
||||||
|
Say ('Test start of ' + $exe + ': still running after 20 s')
|
||||||
|
Stop-Process -Id $p.Id -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $dirs) { Say 'No starsync.exe or watch_together.exe found on Desktop, Downloads, Documents, AppData or Program Files.' }
|
||||||
|
|
||||||
|
# Logs written by the test start.
|
||||||
|
Get-ChildItem -Path "$env:APPDATA\*\*\logs\*" -Include 'app-*' | Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-2) } |
|
||||||
|
ForEach-Object { Copy-Item $_.FullName (Join-Path $o ('after-test_' + $_.Name)) }
|
||||||
|
|
||||||
|
$zip = Join-Path ([Environment]::GetFolderPath('Desktop')) 'starsync-report.zip'
|
||||||
|
Remove-Item -Force $zip
|
||||||
|
Compress-Archive -Path "$o\*" -DestinationPath $zip
|
||||||
|
Set-Clipboard -Path $zip
|
||||||
|
Start-Process explorer.exe -ArgumentList "/select,`"$zip`""
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Done: starsync-report.zip is on your Desktop and copied.' -ForegroundColor Green
|
||||||
|
Write-Host 'Open Discord and press Ctrl+V to send it.' -ForegroundColor Green
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"version": "1.3.0",
|
"version": "1.3.2",
|
||||||
"protocol": 16,
|
"protocol": 16,
|
||||||
"url": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.0/starsync-1.3.0-linux-x64.tar.gz",
|
"url": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.2/starsync-1.3.2-linux-x64.tar.gz",
|
||||||
"notes": "Starsync 1.3.0: the app is now called Starsync (rebuilt inside for stability); a shared countdown to what plays next; watched counts only time you actually watched; faster relay seeks; fixes for exit freezes, Windows shaders and X11 rendering. Everyone must update to keep watching together.",
|
"notes": "Starsync 1.3.2: fixes Windows closing by itself about 3 seconds after every start (1.3.0 and 1.3.1).",
|
||||||
"assets": {
|
"assets": {
|
||||||
"linux-x64": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.0/starsync-1.3.0-linux-x64.tar.gz",
|
"linux-x64": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.2/starsync-1.3.2-linux-x64.tar.gz",
|
||||||
"windows-x64": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.0/starsync-1.3.0-windows-x64.zip"
|
"windows-x64": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.2/starsync-1.3.2-windows-x64.zip"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user