✨ Special display for spotify activity
This commit is contained in:
@@ -467,10 +467,12 @@ final rpcServerStateProvider = StateNotifierProvider<
|
|||||||
activity['details'] ?? activity['assets']?['large_text'];
|
activity['details'] ?? activity['assets']?['large_text'];
|
||||||
var imageSmall = activity['assets']?['small_image'];
|
var imageSmall = activity['assets']?['small_image'];
|
||||||
var imageLarge = activity['assets']?['large_image'];
|
var imageLarge = activity['assets']?['large_image'];
|
||||||
if (imageSmall != null && !imageSmall!.contains(':'))
|
if (imageSmall != null && !imageSmall!.contains(':')) {
|
||||||
imageSmall = 'discord:$imageSmall';
|
imageSmall = 'discord:$imageSmall';
|
||||||
if (imageLarge != null && !imageLarge!.contains(':'))
|
}
|
||||||
|
if (imageLarge != null && !imageLarge!.contains(':')) {
|
||||||
imageLarge = 'discord:$imageLarge';
|
imageLarge = 'discord:$imageLarge';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
final activityData = {
|
final activityData = {
|
||||||
@@ -528,6 +530,13 @@ Future<List<SnPresenceActivity>> presenceActivities(
|
|||||||
Ref ref,
|
Ref ref,
|
||||||
String uname,
|
String uname,
|
||||||
) async {
|
) async {
|
||||||
|
ref.keepAlive();
|
||||||
|
final timer = Timer.periodic(
|
||||||
|
const Duration(minutes: 1),
|
||||||
|
(_) => ref.invalidateSelf(),
|
||||||
|
);
|
||||||
|
ref.onDispose(() => timer.cancel());
|
||||||
|
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
final response = await apiClient.get('/pass/activities/$uname');
|
final response = await apiClient.get('/pass/activities/$uname');
|
||||||
final data = response.data as List<dynamic>;
|
final data = response.data as List<dynamic>;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/activity.dart';
|
import 'package:island/models/activity.dart';
|
||||||
import 'package:island/pods/activity/activity_rpc.dart';
|
import 'package:island/pods/activity/activity_rpc.dart';
|
||||||
|
import 'package:island/widgets/content/image.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@@ -66,7 +67,7 @@ const kPresenceActivityIcons = <IconData>[
|
|||||||
Symbols.running_with_errors,
|
Symbols.running_with_errors,
|
||||||
];
|
];
|
||||||
|
|
||||||
class ActivityPresenceWidget extends ConsumerWidget {
|
class ActivityPresenceWidget extends StatefulWidget {
|
||||||
final String uname;
|
final String uname;
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
final EdgeInsets compactPadding;
|
final EdgeInsets compactPadding;
|
||||||
@@ -78,324 +79,526 @@ class ActivityPresenceWidget extends ConsumerWidget {
|
|||||||
this.compactPadding = EdgeInsets.zero,
|
this.compactPadding = EdgeInsets.zero,
|
||||||
});
|
});
|
||||||
|
|
||||||
List<Widget> _buildDiscordImages(WidgetRef ref, SnPresenceActivity activity) {
|
@override
|
||||||
|
State<ActivityPresenceWidget> createState() => _ActivityPresenceWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActivityPresenceWidgetState extends State<ActivityPresenceWidget>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late AnimationController _progressController;
|
||||||
|
late Animation<double> _progressAnimation;
|
||||||
|
double _startProgress = 0.0;
|
||||||
|
double _endProgress = 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_progressController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
|
);
|
||||||
|
_progressAnimation = Tween<double>(
|
||||||
|
begin: 0.0,
|
||||||
|
end: 0.0,
|
||||||
|
).animate(_progressController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_progressController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildImages(WidgetRef ref, SnPresenceActivity activity) {
|
||||||
final List<Widget> images = [];
|
final List<Widget> images = [];
|
||||||
|
|
||||||
if (activity.largeImage != null &&
|
if (activity.largeImage != null) {
|
||||||
activity.largeImage!.startsWith('discord:')) {
|
if (activity.largeImage!.startsWith('discord:')) {
|
||||||
final key = activity.largeImage!.substring('discord:'.length);
|
final key = activity.largeImage!.substring('discord:'.length);
|
||||||
final urlAsync = ref.watch(discordAssetsUrlProvider(activity, key));
|
final urlAsync = ref.watch(discordAssetsUrlProvider(activity, key));
|
||||||
images.add(
|
images.add(
|
||||||
urlAsync.when(
|
urlAsync.when(
|
||||||
data:
|
data:
|
||||||
(url) =>
|
(url) =>
|
||||||
url != null
|
url != null
|
||||||
? ClipRRect(
|
? ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
loading:
|
loading:
|
||||||
() => const SizedBox(
|
() => const SizedBox(
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
),
|
),
|
||||||
error: (error, stack) => const SizedBox.shrink(),
|
error: (error, stack) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
images.add(
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: UniversalImage(
|
||||||
|
uri: activity.largeImage!,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity.smallImage != null &&
|
if (activity.smallImage != null) {
|
||||||
activity.smallImage!.startsWith('discord:')) {
|
if (activity.smallImage!.startsWith('discord:')) {
|
||||||
final key = activity.smallImage!.substring('discord:'.length);
|
final key = activity.smallImage!.substring('discord:'.length);
|
||||||
final urlAsync = ref.watch(discordAssetsUrlProvider(activity, key));
|
final urlAsync = ref.watch(discordAssetsUrlProvider(activity, key));
|
||||||
images.add(
|
images.add(
|
||||||
urlAsync.when(
|
urlAsync.when(
|
||||||
data:
|
data:
|
||||||
(url) =>
|
(url) =>
|
||||||
url != null
|
url != null
|
||||||
? ClipRRect(
|
? ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
loading:
|
loading:
|
||||||
() => const SizedBox(
|
() => const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
),
|
),
|
||||||
error: (error, stack) => const SizedBox.shrink(),
|
error: (error, stack) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
images.add(
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: UniversalImage(
|
||||||
|
uri: activity.smallImage!,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final activitiesAsync = ref.watch(presenceActivitiesProvider(uname));
|
return Consumer(
|
||||||
|
builder: (BuildContext context, WidgetRef ref, Widget? child) {
|
||||||
|
final activitiesAsync = ref.watch(
|
||||||
|
presenceActivitiesProvider(widget.uname),
|
||||||
|
);
|
||||||
|
|
||||||
if (isCompact) {
|
if (widget.isCompact) {
|
||||||
return activitiesAsync.when(
|
return activitiesAsync.when(
|
||||||
data: (activities) {
|
data: (activities) {
|
||||||
if (activities.isEmpty) return const SizedBox.shrink();
|
if (activities.isEmpty) return const SizedBox.shrink();
|
||||||
final activity = activities.first;
|
final activity = activities.first;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: compactPadding,
|
padding: widget.compactPadding,
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
if (activity.largeImage != null &&
|
if (activity.largeImage != null)
|
||||||
activity.largeImage!.startsWith('discord:'))
|
activity.largeImage!.startsWith('discord:')
|
||||||
ref
|
? ref
|
||||||
.watch(
|
.watch(
|
||||||
discordAssetsUrlProvider(
|
discordAssetsUrlProvider(
|
||||||
activity,
|
activity,
|
||||||
activity.largeImage!.substring('discord:'.length),
|
activity.largeImage!.substring(
|
||||||
),
|
'discord:'.length,
|
||||||
)
|
),
|
||||||
.when(
|
),
|
||||||
data:
|
)
|
||||||
(url) =>
|
.when(
|
||||||
url != null
|
data:
|
||||||
? ClipRRect(
|
(url) =>
|
||||||
borderRadius: BorderRadius.circular(4),
|
url != null
|
||||||
child: CachedNetworkImage(
|
? ClipRRect(
|
||||||
imageUrl: url,
|
borderRadius:
|
||||||
width: 32,
|
BorderRadius.circular(4),
|
||||||
height: 32,
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: url,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
loading:
|
||||||
|
() => const SizedBox(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 1,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const SizedBox.shrink(),
|
error:
|
||||||
loading:
|
(error, stack) => const SizedBox.shrink(),
|
||||||
() => const SizedBox(
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: UniversalImage(
|
||||||
|
uri: activity.largeImage!,
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
child: CircularProgressIndicator(strokeWidth: 1),
|
|
||||||
),
|
),
|
||||||
error: (error, stack) => const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
(activity.title?.isEmpty ?? true)
|
|
||||||
? 'unknown'.tr()
|
|
||||||
: activity.title!,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
).fontSize(13),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
kPresenceActivityTypes[activity.type],
|
|
||||||
).tr().fontSize(11),
|
|
||||||
Icon(
|
|
||||||
kPresenceActivityIcons[activity.type],
|
|
||||||
size: 15,
|
|
||||||
fill: 1,
|
|
||||||
),
|
),
|
||||||
],
|
Expanded(
|
||||||
),
|
child: Column(
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
StreamBuilder(
|
|
||||||
stream: Stream.periodic(const Duration(seconds: 1)),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
|
|
||||||
// Check if lease has expired and refresh if needed
|
|
||||||
if (now.isAfter(activity.leaseExpiresAt)) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
ref.invalidate(presenceActivitiesProvider(uname));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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).fontSize(12);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
error: (error, stack) => const SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return activitiesAsync.when(
|
|
||||||
data:
|
|
||||||
(activities) => Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'activities',
|
|
||||||
).tr().bold().padding(horizontal: 16, vertical: 4),
|
|
||||||
if (activities.isEmpty)
|
|
||||||
Row(
|
|
||||||
spacing: 4,
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.inbox, size: 16),
|
|
||||||
Text('dataEmpty').tr().fontSize(13),
|
|
||||||
],
|
|
||||||
).opacity(0.75).padding(horizontal: 16, bottom: 8),
|
|
||||||
...activities.map((activity) {
|
|
||||||
final dcImages = _buildDiscordImages(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,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (dcImages.isNotEmpty)
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
spacing: 8,
|
|
||||||
children: dcImages,
|
|
||||||
).padding(vertical: 4),
|
|
||||||
Text(
|
Text(
|
||||||
(activity.title?.isEmpty ?? true)
|
(activity.title?.isEmpty ?? true)
|
||||||
? 'unknown'.tr()
|
? 'unknown'.tr()
|
||||||
: activity.title!,
|
: activity.title!,
|
||||||
),
|
maxLines: 1,
|
||||||
],
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
).fontSize(13),
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
Row(
|
||||||
spacing: 4,
|
|
||||||
children: [
|
children: [
|
||||||
Text(kPresenceActivityTypes[activity.type]).tr(),
|
Text(
|
||||||
|
kPresenceActivityTypes[activity.type],
|
||||||
|
).tr().fontSize(11),
|
||||||
Icon(
|
Icon(
|
||||||
kPresenceActivityIcons[activity.type],
|
kPresenceActivityIcons[activity.type],
|
||||||
size: 16,
|
size: 15,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
StreamBuilder(
|
|
||||||
stream: Stream.periodic(const Duration(seconds: 1)),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
|
|
||||||
// Check if lease has expired and refresh if needed
|
|
||||||
if (now.isAfter(activity.leaseExpiresAt)) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((
|
|
||||||
_,
|
|
||||||
) {
|
|
||||||
ref.invalidate(
|
|
||||||
presenceActivitiesProvider(uname),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
Text(activity.subtitle!),
|
|
||||||
if (activity.caption?.isNotEmpty ?? false)
|
|
||||||
Text(activity.caption!),
|
|
||||||
if ((activity.titleUrl?.isNotEmpty ?? false) ||
|
|
||||||
(activity.subtitleUrl?.isNotEmpty ?? false))
|
|
||||||
Row(
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
if (activity.titleUrl != null &&
|
|
||||||
activity.titleUrl!.isNotEmpty)
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed:
|
|
||||||
() =>
|
|
||||||
launchUrlString(activity.titleUrl!),
|
|
||||||
icon: const Icon(Symbols.link, size: 16),
|
|
||||||
label: const Text('Open Title Link'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
textStyle: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (activity.subtitleUrl != null &&
|
|
||||||
activity.subtitleUrl!.isNotEmpty)
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed:
|
|
||||||
() => launchUrlString(
|
|
||||||
activity.subtitleUrl!,
|
|
||||||
),
|
|
||||||
icon: const Icon(Symbols.link, size: 16),
|
|
||||||
label: const Text('Open Subtitle Link'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
textStyle: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
StreamBuilder(
|
||||||
}),
|
stream: Stream.periodic(const Duration(seconds: 1)),
|
||||||
],
|
builder: (context, snapshot) {
|
||||||
).padding(all: 8),
|
final now = DateTime.now();
|
||||||
),
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
if (activity.manualId == 'spotify' &&
|
||||||
error:
|
activity.meta != null) {
|
||||||
(error, stack) =>
|
final meta = activity.meta as Map<String, dynamic>;
|
||||||
Center(child: Text('Error loading activities: $error')),
|
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;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
spacing: 2,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${currentMin.toString().padLeft(2, '0')}:${currentSec.toString().padLeft(2, '0')}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: animatedValue,
|
||||||
|
backgroundColor: Colors.grey.shade300,
|
||||||
|
stopIndicatorColor: Colors.green,
|
||||||
|
trackGap: 0,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(top: 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
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).fontSize(12);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
error: (error, stack) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return activitiesAsync.when(
|
||||||
|
data:
|
||||||
|
(activities) => Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'activities',
|
||||||
|
).tr().bold().padding(horizontal: 16, vertical: 4),
|
||||||
|
if (activities.isEmpty)
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.inbox, size: 16),
|
||||||
|
Text('dataEmpty').tr().fontSize(13),
|
||||||
|
],
|
||||||
|
).opacity(0.75).padding(horizontal: 16, bottom: 8),
|
||||||
|
...activities.map((activity) {
|
||||||
|
final dcImages = _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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
).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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (activity.caption?.isNotEmpty ?? false)
|
||||||
|
Text(activity.caption!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(horizontal: 8);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 8, top: 8, bottom: 16),
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, stack) =>
|
||||||
|
Center(child: Text('Error loading activities: $error')),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ class _EmbedLinkWidgetState extends State<EmbedLinkWidget> {
|
|||||||
Future<void> _checkIfSquare() async {
|
Future<void> _checkIfSquare() async {
|
||||||
if (widget.link.imageUrl == null ||
|
if (widget.link.imageUrl == null ||
|
||||||
widget.link.imageUrl!.isEmpty ||
|
widget.link.imageUrl!.isEmpty ||
|
||||||
widget.link.imageUrl == widget.link.faviconUrl)
|
widget.link.imageUrl == widget.link.faviconUrl) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final image = CachedNetworkImageProvider(widget.link.imageUrl!);
|
final image = CachedNetworkImageProvider(widget.link.imageUrl!);
|
||||||
|
|||||||
Reference in New Issue
Block a user