diff --git a/lib/models/activity.dart b/lib/models/activity.dart index 897433e1..18efacfd 100644 --- a/lib/models/activity.dart +++ b/lib/models/activity.dart @@ -84,6 +84,10 @@ sealed class SnPresenceActivity with _$SnPresenceActivity { required String? title, required String? subtitle, required String? caption, + required String? titleUrl, + required String? subtitleUrl, + required String? smallImage, + required String? largeImage, required Map? meta, required int leaseMinutes, required DateTime leaseExpiresAt, diff --git a/lib/models/activity.freezed.dart b/lib/models/activity.freezed.dart index 99383801..f35da93d 100644 --- a/lib/models/activity.freezed.dart +++ b/lib/models/activity.freezed.dart @@ -1429,7 +1429,7 @@ $SnCheckInResultCopyWith<$Res>? get checkInResult { /// @nodoc mixin _$SnPresenceActivity { - String get id; int get type; String? get manualId; String? get title; String? get subtitle; String? get caption; Map? get meta; int get leaseMinutes; DateTime get leaseExpiresAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; int get type; String? get manualId; String? get title; String? get subtitle; String? get caption; String? get titleUrl; String? get subtitleUrl; String? get smallImage; String? get largeImage; Map? get meta; int get leaseMinutes; DateTime get leaseExpiresAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; /// Create a copy of SnPresenceActivity /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -1442,16 +1442,16 @@ $SnPresenceActivityCopyWith get copyWith => _$SnPresenceActi @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPresenceActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.manualId, manualId) || other.manualId == manualId)&&(identical(other.title, title) || other.title == title)&&(identical(other.subtitle, subtitle) || other.subtitle == subtitle)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.leaseMinutes, leaseMinutes) || other.leaseMinutes == leaseMinutes)&&(identical(other.leaseExpiresAt, leaseExpiresAt) || other.leaseExpiresAt == leaseExpiresAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPresenceActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.manualId, manualId) || other.manualId == manualId)&&(identical(other.title, title) || other.title == title)&&(identical(other.subtitle, subtitle) || other.subtitle == subtitle)&&(identical(other.caption, caption) || other.caption == caption)&&(identical(other.titleUrl, titleUrl) || other.titleUrl == titleUrl)&&(identical(other.subtitleUrl, subtitleUrl) || other.subtitleUrl == subtitleUrl)&&(identical(other.smallImage, smallImage) || other.smallImage == smallImage)&&(identical(other.largeImage, largeImage) || other.largeImage == largeImage)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.leaseMinutes, leaseMinutes) || other.leaseMinutes == leaseMinutes)&&(identical(other.leaseExpiresAt, leaseExpiresAt) || other.leaseExpiresAt == leaseExpiresAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,type,manualId,title,subtitle,caption,const DeepCollectionEquality().hash(meta),leaseMinutes,leaseExpiresAt,accountId,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,type,manualId,title,subtitle,caption,titleUrl,subtitleUrl,smallImage,largeImage,const DeepCollectionEquality().hash(meta),leaseMinutes,leaseExpiresAt,accountId,createdAt,updatedAt,deletedAt); @override String toString() { - return 'SnPresenceActivity(id: $id, type: $type, manualId: $manualId, title: $title, subtitle: $subtitle, caption: $caption, meta: $meta, leaseMinutes: $leaseMinutes, leaseExpiresAt: $leaseExpiresAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnPresenceActivity(id: $id, type: $type, manualId: $manualId, title: $title, subtitle: $subtitle, caption: $caption, titleUrl: $titleUrl, subtitleUrl: $subtitleUrl, smallImage: $smallImage, largeImage: $largeImage, meta: $meta, leaseMinutes: $leaseMinutes, leaseExpiresAt: $leaseExpiresAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -1462,7 +1462,7 @@ abstract mixin class $SnPresenceActivityCopyWith<$Res> { factory $SnPresenceActivityCopyWith(SnPresenceActivity value, $Res Function(SnPresenceActivity) _then) = _$SnPresenceActivityCopyWithImpl; @useResult $Res call({ - String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, int type, String? manualId, String? title, String? subtitle, String? caption, String? titleUrl, String? subtitleUrl, String? smallImage, String? largeImage, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -1479,7 +1479,7 @@ class _$SnPresenceActivityCopyWithImpl<$Res> /// Create a copy of SnPresenceActivity /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? manualId = freezed,Object? title = freezed,Object? subtitle = freezed,Object? caption = freezed,Object? meta = freezed,Object? leaseMinutes = null,Object? leaseExpiresAt = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? manualId = freezed,Object? title = freezed,Object? subtitle = freezed,Object? caption = freezed,Object? titleUrl = freezed,Object? subtitleUrl = freezed,Object? smallImage = freezed,Object? largeImage = freezed,Object? meta = freezed,Object? leaseMinutes = null,Object? leaseExpiresAt = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable @@ -1487,6 +1487,10 @@ as int,manualId: freezed == manualId ? _self.manualId : manualId // ignore: cast as String?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable as String?,subtitle: freezed == subtitle ? _self.subtitle : subtitle // ignore: cast_nullable_to_non_nullable as String?,caption: freezed == caption ? _self.caption : caption // ignore: cast_nullable_to_non_nullable +as String?,titleUrl: freezed == titleUrl ? _self.titleUrl : titleUrl // ignore: cast_nullable_to_non_nullable +as String?,subtitleUrl: freezed == subtitleUrl ? _self.subtitleUrl : subtitleUrl // ignore: cast_nullable_to_non_nullable +as String?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable +as String?,largeImage: freezed == largeImage ? _self.largeImage : largeImage // ignore: cast_nullable_to_non_nullable as String?,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable as Map?,leaseMinutes: null == leaseMinutes ? _self.leaseMinutes : leaseMinutes // ignore: cast_nullable_to_non_nullable as int,leaseExpiresAt: null == leaseExpiresAt ? _self.leaseExpiresAt : leaseExpiresAt // ignore: cast_nullable_to_non_nullable @@ -1576,10 +1580,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, String? titleUrl, String? subtitleUrl, String? smallImage, String? largeImage, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnPresenceActivity() when $default != null: -return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: +return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.titleUrl,_that.subtitleUrl,_that.smallImage,_that.largeImage,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return orElse(); } @@ -1597,10 +1601,10 @@ return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_t /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, String? titleUrl, String? subtitleUrl, String? smallImage, String? largeImage, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; switch (_that) { case _SnPresenceActivity(): -return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} +return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.titleUrl,_that.subtitleUrl,_that.smallImage,_that.largeImage,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} } /// A variant of `when` that fallback to returning `null` /// @@ -1614,10 +1618,10 @@ return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_t /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, String? titleUrl, String? subtitleUrl, String? smallImage, String? largeImage, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; switch (_that) { case _SnPresenceActivity() when $default != null: -return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: +return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.titleUrl,_that.subtitleUrl,_that.smallImage,_that.largeImage,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return null; } @@ -1629,7 +1633,7 @@ return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_t @JsonSerializable() class _SnPresenceActivity implements SnPresenceActivity { - const _SnPresenceActivity({required this.id, required this.type, required this.manualId, required this.title, required this.subtitle, required this.caption, required final Map? meta, required this.leaseMinutes, required this.leaseExpiresAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta; + const _SnPresenceActivity({required this.id, required this.type, required this.manualId, required this.title, required this.subtitle, required this.caption, required this.titleUrl, required this.subtitleUrl, required this.smallImage, required this.largeImage, required final Map? meta, required this.leaseMinutes, required this.leaseExpiresAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta; factory _SnPresenceActivity.fromJson(Map json) => _$SnPresenceActivityFromJson(json); @override final String id; @@ -1638,6 +1642,10 @@ class _SnPresenceActivity implements SnPresenceActivity { @override final String? title; @override final String? subtitle; @override final String? caption; +@override final String? titleUrl; +@override final String? subtitleUrl; +@override final String? smallImage; +@override final String? largeImage; final Map? _meta; @override Map? get meta { final value = _meta; @@ -1667,16 +1675,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPresenceActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.manualId, manualId) || other.manualId == manualId)&&(identical(other.title, title) || other.title == title)&&(identical(other.subtitle, subtitle) || other.subtitle == subtitle)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.leaseMinutes, leaseMinutes) || other.leaseMinutes == leaseMinutes)&&(identical(other.leaseExpiresAt, leaseExpiresAt) || other.leaseExpiresAt == leaseExpiresAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPresenceActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.manualId, manualId) || other.manualId == manualId)&&(identical(other.title, title) || other.title == title)&&(identical(other.subtitle, subtitle) || other.subtitle == subtitle)&&(identical(other.caption, caption) || other.caption == caption)&&(identical(other.titleUrl, titleUrl) || other.titleUrl == titleUrl)&&(identical(other.subtitleUrl, subtitleUrl) || other.subtitleUrl == subtitleUrl)&&(identical(other.smallImage, smallImage) || other.smallImage == smallImage)&&(identical(other.largeImage, largeImage) || other.largeImage == largeImage)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.leaseMinutes, leaseMinutes) || other.leaseMinutes == leaseMinutes)&&(identical(other.leaseExpiresAt, leaseExpiresAt) || other.leaseExpiresAt == leaseExpiresAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,type,manualId,title,subtitle,caption,const DeepCollectionEquality().hash(_meta),leaseMinutes,leaseExpiresAt,accountId,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,type,manualId,title,subtitle,caption,titleUrl,subtitleUrl,smallImage,largeImage,const DeepCollectionEquality().hash(_meta),leaseMinutes,leaseExpiresAt,accountId,createdAt,updatedAt,deletedAt); @override String toString() { - return 'SnPresenceActivity(id: $id, type: $type, manualId: $manualId, title: $title, subtitle: $subtitle, caption: $caption, meta: $meta, leaseMinutes: $leaseMinutes, leaseExpiresAt: $leaseExpiresAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnPresenceActivity(id: $id, type: $type, manualId: $manualId, title: $title, subtitle: $subtitle, caption: $caption, titleUrl: $titleUrl, subtitleUrl: $subtitleUrl, smallImage: $smallImage, largeImage: $largeImage, meta: $meta, leaseMinutes: $leaseMinutes, leaseExpiresAt: $leaseExpiresAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -1687,7 +1695,7 @@ abstract mixin class _$SnPresenceActivityCopyWith<$Res> implements $SnPresenceAc factory _$SnPresenceActivityCopyWith(_SnPresenceActivity value, $Res Function(_SnPresenceActivity) _then) = __$SnPresenceActivityCopyWithImpl; @override @useResult $Res call({ - String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, int type, String? manualId, String? title, String? subtitle, String? caption, String? titleUrl, String? subtitleUrl, String? smallImage, String? largeImage, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -1704,7 +1712,7 @@ class __$SnPresenceActivityCopyWithImpl<$Res> /// Create a copy of SnPresenceActivity /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? manualId = freezed,Object? title = freezed,Object? subtitle = freezed,Object? caption = freezed,Object? meta = freezed,Object? leaseMinutes = null,Object? leaseExpiresAt = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? manualId = freezed,Object? title = freezed,Object? subtitle = freezed,Object? caption = freezed,Object? titleUrl = freezed,Object? subtitleUrl = freezed,Object? smallImage = freezed,Object? largeImage = freezed,Object? meta = freezed,Object? leaseMinutes = null,Object? leaseExpiresAt = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_SnPresenceActivity( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable @@ -1712,6 +1720,10 @@ as int,manualId: freezed == manualId ? _self.manualId : manualId // ignore: cast as String?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable as String?,subtitle: freezed == subtitle ? _self.subtitle : subtitle // ignore: cast_nullable_to_non_nullable as String?,caption: freezed == caption ? _self.caption : caption // ignore: cast_nullable_to_non_nullable +as String?,titleUrl: freezed == titleUrl ? _self.titleUrl : titleUrl // ignore: cast_nullable_to_non_nullable +as String?,subtitleUrl: freezed == subtitleUrl ? _self.subtitleUrl : subtitleUrl // ignore: cast_nullable_to_non_nullable +as String?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable +as String?,largeImage: freezed == largeImage ? _self.largeImage : largeImage // ignore: cast_nullable_to_non_nullable as String?,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable as Map?,leaseMinutes: null == leaseMinutes ? _self.leaseMinutes : leaseMinutes // ignore: cast_nullable_to_non_nullable as int,leaseExpiresAt: null == leaseExpiresAt ? _self.leaseExpiresAt : leaseExpiresAt // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/activity.g.dart b/lib/models/activity.g.dart index 02db2707..1b7d45df 100644 --- a/lib/models/activity.g.dart +++ b/lib/models/activity.g.dart @@ -131,6 +131,10 @@ _SnPresenceActivity _$SnPresenceActivityFromJson(Map json) => title: json['title'] as String?, subtitle: json['subtitle'] as String?, caption: json['caption'] as String?, + titleUrl: json['title_url'] as String?, + subtitleUrl: json['subtitle_url'] as String?, + smallImage: json['small_image'] as String?, + largeImage: json['large_image'] as String?, meta: json['meta'] as Map?, leaseMinutes: (json['lease_minutes'] as num).toInt(), leaseExpiresAt: DateTime.parse(json['lease_expires_at'] as String), @@ -151,6 +155,10 @@ Map _$SnPresenceActivityToJson(_SnPresenceActivity instance) => 'title': instance.title, 'subtitle': instance.subtitle, 'caption': instance.caption, + 'title_url': instance.titleUrl, + 'subtitle_url': instance.subtitleUrl, + 'small_image': instance.smallImage, + 'large_image': instance.largeImage, 'meta': instance.meta, 'lease_minutes': instance.leaseMinutes, 'lease_expires_at': instance.leaseExpiresAt.toIso8601String(), diff --git a/lib/widgets/account/activity_presence.dart b/lib/widgets/account/activity_presence.dart index 9aaf924f..25047988 100644 --- a/lib/widgets/account/activity_presence.dart +++ b/lib/widgets/account/activity_presence.dart @@ -1,9 +1,56 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/activity.dart'; import 'package:island/pods/activity/activity_rpc.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +part 'activity_presence.g.dart'; + +@riverpod +Future?> discordAssets( + Ref ref, + SnPresenceActivity activity, +) async { + final hasDiscordSmall = + activity.smallImage != null && + activity.smallImage!.startsWith('discord:'); + final hasDiscordLarge = + activity.largeImage != null && + activity.largeImage!.startsWith('discord:'); + + if (hasDiscordSmall || hasDiscordLarge) { + final dio = Dio(); + final response = await dio.get( + 'https://discordapp.com/api/oauth2/applications/${activity.manualId}/assets', + ); + final data = response.data as List; + return { + for (final item in data) item['name'] as String: item['id'] as String, + }; + } + + return null; +} + +@riverpod +Future discordAssetsUrl( + Ref ref, + SnPresenceActivity activity, + String key, +) async { + final assets = await ref.watch(discordAssetsProvider(activity).future); + if (assets != null && assets.containsKey(key)) { + final assetId = assets[key]!; + return 'https://cdn.discordapp.com/app-assets/${activity.manualId}/$assetId.png'; + } + return null; +} const kPresenseActivityTypes = [ 'unknown', @@ -12,11 +59,82 @@ const kPresenseActivityTypes = [ 'presenceTypeWorkout', ]; +const kPresenseActivityIcons = [ + Symbols.question_mark_rounded, + Symbols.play_arrow_rounded, + Symbols.music_note_rounded, + Symbols.running_with_errors, +]; + class ActivityPresenceWidget extends ConsumerWidget { final String uname; const ActivityPresenceWidget({super.key, required this.uname}); + List _buildDiscordImages(WidgetRef ref, SnPresenceActivity activity) { + final List images = []; + + if (activity.largeImage != null && + activity.largeImage!.startsWith('discord:')) { + final key = activity.largeImage!.substring('discord:'.length); + final urlAsync = ref.watch(discordAssetsUrlProvider(activity, key)); + images.add( + urlAsync.when( + data: + (url) => + url != null + ? ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: url, + width: 64, + height: 64, + ), + ) + : const SizedBox.shrink(), + loading: + () => const SizedBox( + width: 64, + height: 64, + child: CircularProgressIndicator(strokeWidth: 2), + ), + error: (error, stack) => const SizedBox.shrink(), + ), + ); + } + + if (activity.smallImage != null && + activity.smallImage!.startsWith('discord:')) { + final key = activity.smallImage!.substring('discord:'.length); + final urlAsync = ref.watch(discordAssetsUrlProvider(activity, key)); + images.add( + urlAsync.when( + data: + (url) => + url != null + ? ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: url, + width: 32, + height: 32, + ), + ) + : const SizedBox.shrink(), + loading: + () => const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + error: (error, stack) => const SizedBox.shrink(), + ), + ); + } + + return images; + } + @override Widget build(BuildContext context, WidgetRef ref) { final activitiesAsync = ref.watch(presenceActivitiesProvider(uname)); @@ -40,8 +158,10 @@ class ActivityPresenceWidget extends ConsumerWidget { Text('dataEmpty').tr().fontSize(13), ], ).opacity(0.75).padding(horizontal: 8), - ...activities.map( - (activity) => Card( + ...activities.map((activity) { + final dcImages = _buildDiscordImages(ref, activity); + + return Card( elevation: 0, shape: RoundedRectangleBorder( side: BorderSide(color: Colors.grey.shade300, width: 1), @@ -49,19 +169,49 @@ class ActivityPresenceWidget extends ConsumerWidget { ), margin: EdgeInsets.zero, child: ListTile( - title: Text( - (activity.title?.isEmpty ?? true) - ? 'Untitled Activity' - : activity.title!, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (dcImages.isNotEmpty) + Row( + crossAxisAlignment: CrossAxisAlignment.end, + spacing: 8, + children: dcImages, + ).padding(vertical: 4), + Text( + (activity.title?.isEmpty ?? true) + ? 'unknown'.tr() + : activity.title!, + ), + ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(kPresenseActivityTypes[activity.type]).tr(), + Row( + spacing: 4, + children: [ + Text(kPresenseActivityTypes[activity.type]).tr(), + Icon( + kPresenseActivityIcons[activity.type], + size: 16, + fill: 1, + ), + ], + ), StreamBuilder( stream: Stream.periodic(const Duration(seconds: 1)), builder: (context, snapshot) { - final duration = DateTime.now().difference( + 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( @@ -83,11 +233,38 @@ class ActivityPresenceWidget extends ConsumerWidget { 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), + ), + ), + ], + ), ], ), ), - ), - ), + ); + }), ], ).padding(all: 8), ), diff --git a/lib/widgets/account/activity_presence.g.dart b/lib/widgets/account/activity_presence.g.dart new file mode 100644 index 00000000..b5cea611 --- /dev/null +++ b/lib/widgets/account/activity_presence.g.dart @@ -0,0 +1,287 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'activity_presence.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$discordAssetsHash() => r'3ef8465188059de96cf2ac9660ed3d88910443bf'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [discordAssets]. +@ProviderFor(discordAssets) +const discordAssetsProvider = DiscordAssetsFamily(); + +/// See also [discordAssets]. +class DiscordAssetsFamily extends Family?>> { + /// See also [discordAssets]. + const DiscordAssetsFamily(); + + /// See also [discordAssets]. + DiscordAssetsProvider call(SnPresenceActivity activity) { + return DiscordAssetsProvider(activity); + } + + @override + DiscordAssetsProvider getProviderOverride( + covariant DiscordAssetsProvider provider, + ) { + return call(provider.activity); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'discordAssetsProvider'; +} + +/// See also [discordAssets]. +class DiscordAssetsProvider + extends AutoDisposeFutureProvider?> { + /// See also [discordAssets]. + DiscordAssetsProvider(SnPresenceActivity activity) + : this._internal( + (ref) => discordAssets(ref as DiscordAssetsRef, activity), + from: discordAssetsProvider, + name: r'discordAssetsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$discordAssetsHash, + dependencies: DiscordAssetsFamily._dependencies, + allTransitiveDependencies: + DiscordAssetsFamily._allTransitiveDependencies, + activity: activity, + ); + + DiscordAssetsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.activity, + }) : super.internal(); + + final SnPresenceActivity activity; + + @override + Override overrideWith( + FutureOr?> Function(DiscordAssetsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: DiscordAssetsProvider._internal( + (ref) => create(ref as DiscordAssetsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + activity: activity, + ), + ); + } + + @override + AutoDisposeFutureProviderElement?> createElement() { + return _DiscordAssetsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DiscordAssetsProvider && other.activity == activity; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, activity.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin DiscordAssetsRef on AutoDisposeFutureProviderRef?> { + /// The parameter `activity` of this provider. + SnPresenceActivity get activity; +} + +class _DiscordAssetsProviderElement + extends AutoDisposeFutureProviderElement?> + with DiscordAssetsRef { + _DiscordAssetsProviderElement(super.provider); + + @override + SnPresenceActivity get activity => (origin as DiscordAssetsProvider).activity; +} + +String _$discordAssetsUrlHash() => r'a32f9333c3fb4d50ff88a54a6b8b72fbf5ba3ea1'; + +/// See also [discordAssetsUrl]. +@ProviderFor(discordAssetsUrl) +const discordAssetsUrlProvider = DiscordAssetsUrlFamily(); + +/// See also [discordAssetsUrl]. +class DiscordAssetsUrlFamily extends Family> { + /// See also [discordAssetsUrl]. + const DiscordAssetsUrlFamily(); + + /// See also [discordAssetsUrl]. + DiscordAssetsUrlProvider call(SnPresenceActivity activity, String key) { + return DiscordAssetsUrlProvider(activity, key); + } + + @override + DiscordAssetsUrlProvider getProviderOverride( + covariant DiscordAssetsUrlProvider provider, + ) { + return call(provider.activity, provider.key); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'discordAssetsUrlProvider'; +} + +/// See also [discordAssetsUrl]. +class DiscordAssetsUrlProvider extends AutoDisposeFutureProvider { + /// See also [discordAssetsUrl]. + DiscordAssetsUrlProvider(SnPresenceActivity activity, String key) + : this._internal( + (ref) => discordAssetsUrl(ref as DiscordAssetsUrlRef, activity, key), + from: discordAssetsUrlProvider, + name: r'discordAssetsUrlProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$discordAssetsUrlHash, + dependencies: DiscordAssetsUrlFamily._dependencies, + allTransitiveDependencies: + DiscordAssetsUrlFamily._allTransitiveDependencies, + activity: activity, + key: key, + ); + + DiscordAssetsUrlProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.activity, + required this.key, + }) : super.internal(); + + final SnPresenceActivity activity; + final String key; + + @override + Override overrideWith( + FutureOr Function(DiscordAssetsUrlRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: DiscordAssetsUrlProvider._internal( + (ref) => create(ref as DiscordAssetsUrlRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + activity: activity, + key: key, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _DiscordAssetsUrlProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DiscordAssetsUrlProvider && + other.activity == activity && + other.key == key; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, activity.hashCode); + hash = _SystemHash.combine(hash, key.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin DiscordAssetsUrlRef on AutoDisposeFutureProviderRef { + /// The parameter `activity` of this provider. + SnPresenceActivity get activity; + + /// The parameter `key` of this provider. + String get key; +} + +class _DiscordAssetsUrlProviderElement + extends AutoDisposeFutureProviderElement + with DiscordAssetsUrlRef { + _DiscordAssetsUrlProviderElement(super.provider); + + @override + SnPresenceActivity get activity => + (origin as DiscordAssetsUrlProvider).activity; + @override + String get key => (origin as DiscordAssetsUrlProvider).key; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package