Shows images, url from presense

This commit is contained in:
2025-11-02 00:03:16 +08:00
parent 3de73538c7
commit c093123e3a
5 changed files with 516 additions and 28 deletions

View File

@@ -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<Map<String, String>?> 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<dynamic>;
return {
for (final item in data) item['name'] as String: item['id'] as String,
};
}
return null;
}
@riverpod
Future<String?> 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 = <IconData>[
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<Widget> _buildDiscordImages(WidgetRef ref, SnPresenceActivity activity) {
final List<Widget> 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),
),

View File

@@ -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<AsyncValue<Map<String, String>?>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'discordAssetsProvider';
}
/// See also [discordAssets].
class DiscordAssetsProvider
extends AutoDisposeFutureProvider<Map<String, String>?> {
/// 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<Map<String, String>?> 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<Map<String, String>?> 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<Map<String, String>?> {
/// The parameter `activity` of this provider.
SnPresenceActivity get activity;
}
class _DiscordAssetsProviderElement
extends AutoDisposeFutureProviderElement<Map<String, String>?>
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<AsyncValue<String?>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'discordAssetsUrlProvider';
}
/// See also [discordAssetsUrl].
class DiscordAssetsUrlProvider extends AutoDisposeFutureProvider<String?> {
/// 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<String?> 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<String?> 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<String?> {
/// The parameter `activity` of this provider.
SnPresenceActivity get activity;
/// The parameter `key` of this provider.
String get key;
}
class _DiscordAssetsUrlProviderElement
extends AutoDisposeFutureProviderElement<String?>
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