✨ Able to render offsite media
This commit is contained in:
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:island/models/activitypub.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class ActorAvatarWidget extends StatelessWidget {
|
||||
class ActorPictureWidget extends StatelessWidget {
|
||||
final SnActivityPubActor actor;
|
||||
final double radius;
|
||||
|
||||
const ActorAvatarWidget({super.key, required this.actor, this.radius = 16});
|
||||
const ActorPictureWidget({super.key, required this.actor, this.radius = 16});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -29,10 +29,12 @@ class ActorAvatarWidget extends StatelessWidget {
|
||||
backgroundImage: CachedNetworkImageProvider(avatarUrl),
|
||||
radius: radius,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Icon(
|
||||
Symbols.person,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
child: avatarUrl.isNotEmpty
|
||||
? null
|
||||
: Icon(
|
||||
Symbols.person,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
|
||||
@@ -4,11 +4,13 @@ import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
void showSnackBar(String message, {SnackBarAction? action}) {
|
||||
final context = globalOverlay.currentState!.context;
|
||||
@@ -373,3 +375,20 @@ Future<bool> showConfirmAlert(
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Future<void> openExternalLink(Uri url, WidgetRef ref) async {
|
||||
final whitelistDomains = ['solian.app', 'solsynth.dev'];
|
||||
if (whitelistDomains.any(
|
||||
(domain) => url.host == domain || url.host.endsWith('.$domain'),
|
||||
)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
final value = await showConfirmAlert(
|
||||
'openLinkConfirmDescription'.tr(args: [url.toString()]),
|
||||
'openLinkConfirm'.tr(),
|
||||
);
|
||||
if (value) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class CloudFileLightbox extends HookConsumerWidget {
|
||||
controller: photoViewController,
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: heroTag),
|
||||
imageProvider: CloudImageWidget.provider(
|
||||
fileId: item.id,
|
||||
file: item,
|
||||
serverUrl: serverUrl,
|
||||
original: showOriginal.value,
|
||||
),
|
||||
@@ -118,20 +118,21 @@ class CloudFileLightbox extends HookConsumerWidget {
|
||||
onPressed: showInfoSheet,
|
||||
shadows: WhiteShadows.standard,
|
||||
),
|
||||
FileActionButton.more(
|
||||
onPressed: () {
|
||||
final router = GoRouter.of(context);
|
||||
Navigator.of(context).pop(context);
|
||||
Future(() {
|
||||
router.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
});
|
||||
},
|
||||
shadows: WhiteShadows.standard,
|
||||
),
|
||||
if (item.url != null)
|
||||
FileActionButton.more(
|
||||
onPressed: () {
|
||||
final router = GoRouter.of(context);
|
||||
Navigator.of(context).pop(context);
|
||||
Future(() {
|
||||
router.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
});
|
||||
},
|
||||
shadows: WhiteShadows.standard,
|
||||
),
|
||||
],
|
||||
showExtraOnLeft: true,
|
||||
),
|
||||
|
||||
@@ -41,7 +41,7 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
appSettingsProvider.select((s) => s.dataSavingMode),
|
||||
);
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final uri = '$serverUrl/drive/files/${item.id}';
|
||||
final uri = item.url ?? '$serverUrl/drive/files/${item.id}';
|
||||
|
||||
final unlocked = useState(false);
|
||||
|
||||
@@ -529,7 +529,7 @@ class CloudImageWidget extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final uri = '$serverUrl/drive/files/${file?.id ?? fileId}';
|
||||
final uri = file?.url ?? '$serverUrl/drive/files/${file?.id ?? fileId}';
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
@@ -540,13 +540,15 @@ class CloudImageWidget extends ConsumerWidget {
|
||||
}
|
||||
|
||||
static ImageProvider provider({
|
||||
required String fileId,
|
||||
required SnCloudFile file,
|
||||
required String serverUrl,
|
||||
bool original = false,
|
||||
}) {
|
||||
final uri = original
|
||||
? '$serverUrl/drive/files/$fileId?original=true'
|
||||
: '$serverUrl/drive/files/$fileId';
|
||||
final uri =
|
||||
file.url ??
|
||||
(original
|
||||
? '$serverUrl/drive/files/${file.id}?original=true'
|
||||
: '$serverUrl/drive/files/${file.id}');
|
||||
return CachedNetworkImageProvider(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ class ImageFileContent extends HookConsumerWidget {
|
||||
),
|
||||
controller: photoViewController,
|
||||
imageProvider: CloudImageWidget.provider(
|
||||
fileId: item.id,
|
||||
file: item,
|
||||
serverUrl: ref.watch(serverUrlProvider),
|
||||
original: showOriginal.value,
|
||||
),
|
||||
|
||||
@@ -21,7 +21,6 @@ import 'package:markdown/markdown.dart' as markdown;
|
||||
import 'package:markdown_widget/markdown_widget.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'image.dart';
|
||||
@@ -139,7 +138,7 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
style:
|
||||
linkStyle ??
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
onTap: (href) {
|
||||
onTap: (href) async {
|
||||
final url = Uri.tryParse(href);
|
||||
if (url != null) {
|
||||
if (url.scheme == 'solian') {
|
||||
@@ -147,22 +146,7 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
context.push(fullPath);
|
||||
return;
|
||||
}
|
||||
final whitelistDomains = ['solian.app', 'solsynth.dev'];
|
||||
if (whitelistDomains.any(
|
||||
(domain) =>
|
||||
url.host == domain || url.host.endsWith('.$domain'),
|
||||
)) {
|
||||
launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
return;
|
||||
}
|
||||
showConfirmAlert(
|
||||
'openLinkConfirmDescription'.tr(args: [url.toString()]),
|
||||
'openLinkConfirm'.tr(),
|
||||
).then((value) {
|
||||
if (value) {
|
||||
launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
});
|
||||
await openExternalLink(url, ref);
|
||||
} else {
|
||||
showSnackBar(
|
||||
'brokenLink'.tr(args: [href]),
|
||||
|
||||
@@ -146,7 +146,7 @@ class PostReplyPreview extends HookConsumerWidget {
|
||||
}
|
||||
// Handle actor case
|
||||
if (post.actor != null) {
|
||||
return ActorAvatarWidget(actor: post.actor!, radius: radius);
|
||||
return ActorPictureWidget(actor: post.actor!, radius: radius);
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
@@ -452,7 +452,7 @@ class ReferencedPostWidget extends StatelessWidget {
|
||||
}
|
||||
// Handle actor case
|
||||
if (post.actor != null) {
|
||||
return ActorAvatarWidget(actor: post.actor!, radius: radius);
|
||||
return ActorPictureWidget(actor: post.actor!, radius: radius);
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
@@ -657,7 +657,7 @@ class ReferencedPostWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class PostHeader extends StatelessWidget {
|
||||
class PostHeader extends HookConsumerWidget {
|
||||
final SnPost item;
|
||||
final bool isFullPost;
|
||||
final Widget? trailing;
|
||||
@@ -695,7 +695,7 @@ class PostHeader extends StatelessWidget {
|
||||
}
|
||||
// Handle actor case
|
||||
if (post.actor != null) {
|
||||
return ActorAvatarWidget(actor: post.actor!, radius: radius);
|
||||
return ActorPictureWidget(actor: post.actor!, radius: radius);
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
@@ -746,7 +746,7 @@ class PostHeader extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -756,12 +756,12 @@ class PostHeader extends StatelessWidget {
|
||||
GestureDetector(
|
||||
onTap: isInteractive && _getPublisherName(item) != null
|
||||
? () {
|
||||
context.pushNamed(
|
||||
'publisherProfile',
|
||||
pathParameters: {
|
||||
'name': _getPublisherName(item) as String,
|
||||
},
|
||||
);
|
||||
if (item.publisher != null) {
|
||||
context.pushNamed(
|
||||
'publisherProfile',
|
||||
pathParameters: {'name': item.publisher!.name},
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: _buildProfilePicture(context, item, radius: 16),
|
||||
|
||||
Reference in New Issue
Block a user