💄 Shows listening activities are from spotfiy
This commit is contained in:
		@@ -393,202 +393,234 @@ class _ActivityPresenceWidgetState extends State<ActivityPresenceWidget>
 | 
			
		||||
                        ],
 | 
			
		||||
                      ).opacity(0.75).padding(horizontal: 16, bottom: 8),
 | 
			
		||||
                    ...activities.map((activity) {
 | 
			
		||||
                      final dcImages = _buildImages(ref, activity);
 | 
			
		||||
                      final images = _buildImages(ref, activity);
 | 
			
		||||
 | 
			
		||||
                      return Card(
 | 
			
		||||
                        elevation: 0,
 | 
			
		||||
                        shape: RoundedRectangleBorder(
 | 
			
		||||
                          side: BorderSide(
 | 
			
		||||
                            color: Colors.grey.shade300,
 | 
			
		||||
                            width: 1,
 | 
			
		||||
                          ),
 | 
			
		||||
                          borderRadius: BorderRadius.circular(8),
 | 
			
		||||
                        ),
 | 
			
		||||
                        margin: EdgeInsets.zero,
 | 
			
		||||
                        child: ListTile(
 | 
			
		||||
                          title: Column(
 | 
			
		||||
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              if (dcImages.isNotEmpty)
 | 
			
		||||
                                Row(
 | 
			
		||||
                                  crossAxisAlignment: CrossAxisAlignment.end,
 | 
			
		||||
                                  spacing: 8,
 | 
			
		||||
                                  children: dcImages,
 | 
			
		||||
                                ).padding(vertical: 4),
 | 
			
		||||
                              Row(
 | 
			
		||||
                                spacing: 2,
 | 
			
		||||
                      return Stack(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Card(
 | 
			
		||||
                            elevation: 0,
 | 
			
		||||
                            shape: RoundedRectangleBorder(
 | 
			
		||||
                              side: BorderSide(
 | 
			
		||||
                                color: Colors.grey.shade300,
 | 
			
		||||
                                width: 1,
 | 
			
		||||
                              ),
 | 
			
		||||
                              borderRadius: BorderRadius.circular(8),
 | 
			
		||||
                            ),
 | 
			
		||||
                            margin: EdgeInsets.zero,
 | 
			
		||||
                            child: ListTile(
 | 
			
		||||
                              title: Column(
 | 
			
		||||
                                crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Flexible(
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      (activity.title?.isEmpty ?? true)
 | 
			
		||||
                                          ? 'unknown'.tr()
 | 
			
		||||
                                          : activity.title!,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  if (activity.titleUrl != null &&
 | 
			
		||||
                                      activity.titleUrl!.isNotEmpty)
 | 
			
		||||
                                    IconButton(
 | 
			
		||||
                                      onPressed: () {
 | 
			
		||||
                                        launchUrlString(activity.titleUrl!);
 | 
			
		||||
                                      },
 | 
			
		||||
                                      icon: const Icon(Symbols.launch_rounded),
 | 
			
		||||
                                      iconSize: 16,
 | 
			
		||||
                                      padding: EdgeInsets.all(4),
 | 
			
		||||
                                      constraints: const BoxConstraints(
 | 
			
		||||
                                        maxWidth: 28,
 | 
			
		||||
                                        maxHeight: 28,
 | 
			
		||||
                                  if (images.isNotEmpty)
 | 
			
		||||
                                    Row(
 | 
			
		||||
                                      crossAxisAlignment:
 | 
			
		||||
                                          CrossAxisAlignment.end,
 | 
			
		||||
                                      spacing: 8,
 | 
			
		||||
                                      children: images,
 | 
			
		||||
                                    ).padding(vertical: 4),
 | 
			
		||||
                                  Row(
 | 
			
		||||
                                    spacing: 2,
 | 
			
		||||
                                    children: [
 | 
			
		||||
                                      Flexible(
 | 
			
		||||
                                        child: Text(
 | 
			
		||||
                                          (activity.title?.isEmpty ?? true)
 | 
			
		||||
                                              ? 'unknown'.tr()
 | 
			
		||||
                                              : activity.title!,
 | 
			
		||||
                                        ),
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                      if (activity.titleUrl != null &&
 | 
			
		||||
                                          activity.titleUrl!.isNotEmpty)
 | 
			
		||||
                                        IconButton(
 | 
			
		||||
                                          onPressed: () {
 | 
			
		||||
                                            launchUrlString(activity.titleUrl!);
 | 
			
		||||
                                          },
 | 
			
		||||
                                          icon: const Icon(
 | 
			
		||||
                                            Symbols.launch_rounded,
 | 
			
		||||
                                          ),
 | 
			
		||||
                                          iconSize: 16,
 | 
			
		||||
                                          padding: EdgeInsets.all(4),
 | 
			
		||||
                                          constraints: const BoxConstraints(
 | 
			
		||||
                                            maxWidth: 28,
 | 
			
		||||
                                            maxHeight: 28,
 | 
			
		||||
                                          ),
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
                          subtitle: Column(
 | 
			
		||||
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              Row(
 | 
			
		||||
                                spacing: 4,
 | 
			
		||||
                              subtitle: Column(
 | 
			
		||||
                                crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Text(
 | 
			
		||||
                                    kPresenceActivityTypes[activity.type],
 | 
			
		||||
                                  ).tr(),
 | 
			
		||||
                                  Icon(
 | 
			
		||||
                                    kPresenceActivityIcons[activity.type],
 | 
			
		||||
                                    size: 16,
 | 
			
		||||
                                    fill: 1,
 | 
			
		||||
                                  Row(
 | 
			
		||||
                                    spacing: 4,
 | 
			
		||||
                                    children: [
 | 
			
		||||
                                      Text(
 | 
			
		||||
                                        kPresenceActivityTypes[activity.type],
 | 
			
		||||
                                      ).tr(),
 | 
			
		||||
                                      Icon(
 | 
			
		||||
                                        kPresenceActivityIcons[activity.type],
 | 
			
		||||
                                        size: 16,
 | 
			
		||||
                                        fill: 1,
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                              if (activity.manualId == 'spotify' &&
 | 
			
		||||
                                  activity.meta != null)
 | 
			
		||||
                                StreamBuilder(
 | 
			
		||||
                                  stream: Stream.periodic(
 | 
			
		||||
                                    const Duration(seconds: 1),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  builder: (context, snapshot) {
 | 
			
		||||
                                    final now = DateTime.now();
 | 
			
		||||
                                    final meta =
 | 
			
		||||
                                        activity.meta as Map<String, dynamic>;
 | 
			
		||||
                                    final progressMs =
 | 
			
		||||
                                        meta['progress_ms'] as int? ?? 0;
 | 
			
		||||
                                    final durationMs =
 | 
			
		||||
                                        meta['track_duration_ms'] as int? ?? 1;
 | 
			
		||||
                                    final elapsed =
 | 
			
		||||
                                        now
 | 
			
		||||
                                            .difference(activity.createdAt)
 | 
			
		||||
                                            .inMilliseconds;
 | 
			
		||||
                                    final currentProgressMs =
 | 
			
		||||
                                        (progressMs + elapsed) % durationMs;
 | 
			
		||||
                                    final progressValue =
 | 
			
		||||
                                        currentProgressMs / durationMs;
 | 
			
		||||
                                    if (progressValue != _endProgress) {
 | 
			
		||||
                                      _startProgress = _endProgress;
 | 
			
		||||
                                      _endProgress = progressValue;
 | 
			
		||||
                                      _progressAnimation = Tween<double>(
 | 
			
		||||
                                        begin: _startProgress,
 | 
			
		||||
                                        end: _endProgress,
 | 
			
		||||
                                      ).animate(_progressController);
 | 
			
		||||
                                      _progressController.forward(from: 0.0);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    return AnimatedBuilder(
 | 
			
		||||
                                      animation: _progressAnimation,
 | 
			
		||||
                                      builder: (context, child) {
 | 
			
		||||
                                        final animatedValue =
 | 
			
		||||
                                            _progressAnimation.value;
 | 
			
		||||
                                        final animatedProgressMs =
 | 
			
		||||
                                            (animatedValue * durationMs)
 | 
			
		||||
                                                .toInt();
 | 
			
		||||
                                        final currentMin =
 | 
			
		||||
                                            animatedProgressMs ~/ 60000;
 | 
			
		||||
                                        final currentSec =
 | 
			
		||||
                                            (animatedProgressMs % 60000) ~/
 | 
			
		||||
                                            1000;
 | 
			
		||||
                                        final totalMin = durationMs ~/ 60000;
 | 
			
		||||
                                        final totalSec =
 | 
			
		||||
                                            (durationMs % 60000) ~/ 1000;
 | 
			
		||||
                                        return Column(
 | 
			
		||||
                                          crossAxisAlignment:
 | 
			
		||||
                                              CrossAxisAlignment.start,
 | 
			
		||||
                                          spacing: 4,
 | 
			
		||||
                                          children: [
 | 
			
		||||
                                            LinearProgressIndicator(
 | 
			
		||||
                                              value: animatedValue,
 | 
			
		||||
                                              backgroundColor:
 | 
			
		||||
                                                  Colors.grey.shade300,
 | 
			
		||||
                                              trackGap: 0,
 | 
			
		||||
                                              stopIndicatorColor: Colors.green,
 | 
			
		||||
                                              valueColor:
 | 
			
		||||
                                                  AlwaysStoppedAnimation<Color>(
 | 
			
		||||
                                                    Colors.green,
 | 
			
		||||
                                  if (activity.manualId == 'spotify' &&
 | 
			
		||||
                                      activity.meta != null)
 | 
			
		||||
                                    StreamBuilder(
 | 
			
		||||
                                      stream: Stream.periodic(
 | 
			
		||||
                                        const Duration(seconds: 1),
 | 
			
		||||
                                      ),
 | 
			
		||||
                                      builder: (context, snapshot) {
 | 
			
		||||
                                        final now = DateTime.now();
 | 
			
		||||
                                        final meta =
 | 
			
		||||
                                            activity.meta
 | 
			
		||||
                                                as Map<String, dynamic>;
 | 
			
		||||
                                        final progressMs =
 | 
			
		||||
                                            meta['progress_ms'] as int? ?? 0;
 | 
			
		||||
                                        final durationMs =
 | 
			
		||||
                                            meta['track_duration_ms'] as int? ??
 | 
			
		||||
                                            1;
 | 
			
		||||
                                        final elapsed =
 | 
			
		||||
                                            now
 | 
			
		||||
                                                .difference(activity.createdAt)
 | 
			
		||||
                                                .inMilliseconds;
 | 
			
		||||
                                        final currentProgressMs =
 | 
			
		||||
                                            (progressMs + elapsed) % durationMs;
 | 
			
		||||
                                        final progressValue =
 | 
			
		||||
                                            currentProgressMs / durationMs;
 | 
			
		||||
                                        if (progressValue != _endProgress) {
 | 
			
		||||
                                          _startProgress = _endProgress;
 | 
			
		||||
                                          _endProgress = progressValue;
 | 
			
		||||
                                          _progressAnimation = Tween<double>(
 | 
			
		||||
                                            begin: _startProgress,
 | 
			
		||||
                                            end: _endProgress,
 | 
			
		||||
                                          ).animate(_progressController);
 | 
			
		||||
                                          _progressController.forward(
 | 
			
		||||
                                            from: 0.0,
 | 
			
		||||
                                          );
 | 
			
		||||
                                        }
 | 
			
		||||
                                        return AnimatedBuilder(
 | 
			
		||||
                                          animation: _progressAnimation,
 | 
			
		||||
                                          builder: (context, child) {
 | 
			
		||||
                                            final animatedValue =
 | 
			
		||||
                                                _progressAnimation.value;
 | 
			
		||||
                                            final animatedProgressMs =
 | 
			
		||||
                                                (animatedValue * durationMs)
 | 
			
		||||
                                                    .toInt();
 | 
			
		||||
                                            final currentMin =
 | 
			
		||||
                                                animatedProgressMs ~/ 60000;
 | 
			
		||||
                                            final currentSec =
 | 
			
		||||
                                                (animatedProgressMs % 60000) ~/
 | 
			
		||||
                                                1000;
 | 
			
		||||
                                            final totalMin =
 | 
			
		||||
                                                durationMs ~/ 60000;
 | 
			
		||||
                                            final totalSec =
 | 
			
		||||
                                                (durationMs % 60000) ~/ 1000;
 | 
			
		||||
                                            return Column(
 | 
			
		||||
                                              crossAxisAlignment:
 | 
			
		||||
                                                  CrossAxisAlignment.start,
 | 
			
		||||
                                              spacing: 4,
 | 
			
		||||
                                              children: [
 | 
			
		||||
                                                LinearProgressIndicator(
 | 
			
		||||
                                                  value: animatedValue,
 | 
			
		||||
                                                  backgroundColor:
 | 
			
		||||
                                                      Colors.grey.shade300,
 | 
			
		||||
                                                  trackGap: 0,
 | 
			
		||||
                                                  stopIndicatorColor:
 | 
			
		||||
                                                      Colors.green,
 | 
			
		||||
                                                  valueColor:
 | 
			
		||||
                                                      AlwaysStoppedAnimation<
 | 
			
		||||
                                                        Color
 | 
			
		||||
                                                      >(Colors.green),
 | 
			
		||||
                                                ).padding(top: 3),
 | 
			
		||||
                                                Text(
 | 
			
		||||
                                                  '${currentMin.toString().padLeft(2, '0')}:${currentSec.toString().padLeft(2, '0')} / ${totalMin.toString().padLeft(2, '0')}:${totalSec.toString().padLeft(2, '0')}',
 | 
			
		||||
                                                  style: TextStyle(
 | 
			
		||||
                                                    fontSize: 12,
 | 
			
		||||
                                                    color: Colors.green,
 | 
			
		||||
                                                  ),
 | 
			
		||||
                                            ).padding(top: 3),
 | 
			
		||||
                                            Text(
 | 
			
		||||
                                              '${currentMin.toString().padLeft(2, '0')}:${currentSec.toString().padLeft(2, '0')} / ${totalMin.toString().padLeft(2, '0')}:${totalSec.toString().padLeft(2, '0')}',
 | 
			
		||||
                                              style: TextStyle(
 | 
			
		||||
                                                fontSize: 12,
 | 
			
		||||
                                                color: Colors.green,
 | 
			
		||||
                                              ),
 | 
			
		||||
                                            ),
 | 
			
		||||
                                          ],
 | 
			
		||||
                                                ),
 | 
			
		||||
                                              ],
 | 
			
		||||
                                            );
 | 
			
		||||
                                          },
 | 
			
		||||
                                        );
 | 
			
		||||
                                      },
 | 
			
		||||
                                    );
 | 
			
		||||
                                  },
 | 
			
		||||
                                )
 | 
			
		||||
                              else
 | 
			
		||||
                                StreamBuilder(
 | 
			
		||||
                                  stream: Stream.periodic(
 | 
			
		||||
                                    const Duration(seconds: 1),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  builder: (context, snapshot) {
 | 
			
		||||
                                    final now = DateTime.now();
 | 
			
		||||
 | 
			
		||||
                                    final duration = now.difference(
 | 
			
		||||
                                      activity.createdAt,
 | 
			
		||||
                                    );
 | 
			
		||||
                                    final hours = duration.inHours
 | 
			
		||||
                                        .toString()
 | 
			
		||||
                                        .padLeft(2, '0');
 | 
			
		||||
                                    final minutes = (duration.inMinutes % 60)
 | 
			
		||||
                                        .toString()
 | 
			
		||||
                                        .padLeft(2, '0');
 | 
			
		||||
                                    final seconds = (duration.inSeconds % 60)
 | 
			
		||||
                                        .toString()
 | 
			
		||||
                                        .padLeft(2, '0');
 | 
			
		||||
                                    return Text(
 | 
			
		||||
                                      '$hours:$minutes:$seconds',
 | 
			
		||||
                                    ).textColor(Colors.green);
 | 
			
		||||
                                  },
 | 
			
		||||
                                ),
 | 
			
		||||
                              if (activity.subtitle?.isNotEmpty ?? false)
 | 
			
		||||
                                Row(
 | 
			
		||||
                                  spacing: 2,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    Flexible(child: Text(activity.subtitle!)),
 | 
			
		||||
                                    if (activity.titleUrl != null &&
 | 
			
		||||
                                        activity.titleUrl!.isNotEmpty)
 | 
			
		||||
                                      IconButton(
 | 
			
		||||
                                        onPressed: () {
 | 
			
		||||
                                          launchUrlString(activity.titleUrl!);
 | 
			
		||||
                                        },
 | 
			
		||||
                                        icon: const Icon(
 | 
			
		||||
                                          Symbols.launch_rounded,
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        iconSize: 16,
 | 
			
		||||
                                        padding: EdgeInsets.all(4),
 | 
			
		||||
                                        constraints: const BoxConstraints(
 | 
			
		||||
                                          maxWidth: 28,
 | 
			
		||||
                                          maxHeight: 28,
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    )
 | 
			
		||||
                                  else
 | 
			
		||||
                                    StreamBuilder(
 | 
			
		||||
                                      stream: Stream.periodic(
 | 
			
		||||
                                        const Duration(seconds: 1),
 | 
			
		||||
                                      ),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                      builder: (context, snapshot) {
 | 
			
		||||
                                        final now = DateTime.now();
 | 
			
		||||
 | 
			
		||||
                                        final duration = now.difference(
 | 
			
		||||
                                          activity.createdAt,
 | 
			
		||||
                                        );
 | 
			
		||||
                                        final hours = duration.inHours
 | 
			
		||||
                                            .toString()
 | 
			
		||||
                                            .padLeft(2, '0');
 | 
			
		||||
                                        final minutes = (duration.inMinutes %
 | 
			
		||||
                                                60)
 | 
			
		||||
                                            .toString()
 | 
			
		||||
                                            .padLeft(2, '0');
 | 
			
		||||
                                        final seconds = (duration.inSeconds %
 | 
			
		||||
                                                60)
 | 
			
		||||
                                            .toString()
 | 
			
		||||
                                            .padLeft(2, '0');
 | 
			
		||||
                                        return Text(
 | 
			
		||||
                                          '$hours:$minutes:$seconds',
 | 
			
		||||
                                        ).textColor(Colors.green);
 | 
			
		||||
                                      },
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  if (activity.subtitle?.isNotEmpty ?? false)
 | 
			
		||||
                                    Row(
 | 
			
		||||
                                      spacing: 2,
 | 
			
		||||
                                      children: [
 | 
			
		||||
                                        Flexible(
 | 
			
		||||
                                          child: Text(activity.subtitle!),
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        if (activity.titleUrl != null &&
 | 
			
		||||
                                            activity.titleUrl!.isNotEmpty)
 | 
			
		||||
                                          IconButton(
 | 
			
		||||
                                            onPressed: () {
 | 
			
		||||
                                              launchUrlString(
 | 
			
		||||
                                                activity.titleUrl!,
 | 
			
		||||
                                              );
 | 
			
		||||
                                            },
 | 
			
		||||
                                            icon: const Icon(
 | 
			
		||||
                                              Symbols.launch_rounded,
 | 
			
		||||
                                            ),
 | 
			
		||||
                                            iconSize: 16,
 | 
			
		||||
                                            padding: EdgeInsets.all(4),
 | 
			
		||||
                                            constraints: const BoxConstraints(
 | 
			
		||||
                                              maxWidth: 28,
 | 
			
		||||
                                              maxHeight: 28,
 | 
			
		||||
                                            ),
 | 
			
		||||
                                          ),
 | 
			
		||||
                                      ],
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  if (activity.caption?.isNotEmpty ?? false)
 | 
			
		||||
                                    Text(activity.caption!),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ).padding(horizontal: 8),
 | 
			
		||||
                          if (activity.manualId == 'spotify')
 | 
			
		||||
                            Positioned(
 | 
			
		||||
                              top: 16,
 | 
			
		||||
                              right: 24,
 | 
			
		||||
                              child: Tooltip(
 | 
			
		||||
                                message: 'Listening on Spotify',
 | 
			
		||||
                                child: Image.asset(
 | 
			
		||||
                                  'assets/images/oidc/spotify.png',
 | 
			
		||||
                                  width: 24,
 | 
			
		||||
                                  height: 24,
 | 
			
		||||
                                ),
 | 
			
		||||
                              if (activity.caption?.isNotEmpty ?? false)
 | 
			
		||||
                                Text(activity.caption!),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ).padding(horizontal: 8);
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      );
 | 
			
		||||
                    }),
 | 
			
		||||
                  ],
 | 
			
		||||
                ).padding(horizontal: 8, top: 8, bottom: 16),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user