Compare commits
2 Commits
b750cc3c67
...
80a66136ce
Author | SHA1 | Date | |
---|---|---|---|
80a66136ce | |||
1f8d47f6c3 |
@ -444,5 +444,6 @@
|
|||||||
"postImageShareAds": "Explore posts on the Solar Network",
|
"postImageShareAds": "Explore posts on the Solar Network",
|
||||||
"postShare": "Share",
|
"postShare": "Share",
|
||||||
"postShareImage": "Share via Image",
|
"postShareImage": "Share via Image",
|
||||||
"appInitializing": "Initializing"
|
"appInitializing": "Initializing",
|
||||||
|
"poweredBy": "Powered by {}"
|
||||||
}
|
}
|
||||||
|
@ -442,5 +442,6 @@
|
|||||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||||
"postShare": "分享",
|
"postShare": "分享",
|
||||||
"postShareImage": "分享帖图",
|
"postShareImage": "分享帖图",
|
||||||
"appInitializing": "正在初始化"
|
"appInitializing": "正在初始化",
|
||||||
|
"poweredBy": "由 {} 提供支持"
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/firebase_options.dart';
|
import 'package:surface/firebase_options.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
|
import 'package:surface/providers/link_preview.dart';
|
||||||
import 'package:surface/providers/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
import 'package:surface/providers/notification.dart';
|
import 'package:surface/providers/notification.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
@ -92,6 +93,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
||||||
|
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||||
@ -111,7 +113,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AppDelegate extends StatelessWidget {
|
class _AppDelegate extends StatelessWidget {
|
||||||
const _AppDelegate({super.key});
|
const _AppDelegate();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -134,7 +136,10 @@ class _AppDelegate extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
routerConfig: appRouter,
|
routerConfig: appRouter,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return _AppSplashScreen(child: child!);
|
return _AppSplashScreen(
|
||||||
|
key: const Key('global-splash-screen'),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -187,7 +192,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
body: Container(
|
body: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 180),
|
constraints: const BoxConstraints(maxWidth: 180),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Image.asset("assets/icon/icon.png", width: 64, height: 64),
|
Image.asset("assets/icon/icon.png", width: 64, height: 64),
|
||||||
|
35
lib/providers/link_preview.dart
Normal file
35
lib/providers/link_preview.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/link.dart';
|
||||||
|
|
||||||
|
class SnLinkPreviewProvider {
|
||||||
|
late final SnNetworkProvider _sn;
|
||||||
|
|
||||||
|
final Map<String, SnLinkMeta> _cache = {};
|
||||||
|
|
||||||
|
SnLinkPreviewProvider(BuildContext context) {
|
||||||
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnLinkMeta?> getLinkMeta(String url) async {
|
||||||
|
final b64 = utf8.fuse(base64Url);
|
||||||
|
final target = b64.encode(url);
|
||||||
|
if (_cache.containsKey(target)) return _cache[target];
|
||||||
|
|
||||||
|
log('[LinkPreview] Fetching $url ($target)');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final resp = await _sn.client.get('/cgi/re/link/$target');
|
||||||
|
final meta = SnLinkMeta.fromJson(resp.data);
|
||||||
|
_cache[url] = meta;
|
||||||
|
return meta;
|
||||||
|
} catch (err) {
|
||||||
|
log('[LinkPreview] Failed to fetch $url ($target)...');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tryConnect() async {
|
Future<void> tryConnect() async {
|
||||||
|
if (isConnected) return;
|
||||||
if (!_ua.isAuthorized) return;
|
if (!_ua.isAuthorized) return;
|
||||||
|
|
||||||
log('[WebSocket] Connecting to the server...');
|
log('[WebSocket] Connecting to the server...');
|
||||||
@ -76,6 +77,7 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
if (conn != null) {
|
if (conn != null) {
|
||||||
conn!.sink.close();
|
conn!.sink.close();
|
||||||
}
|
}
|
||||||
|
conn = null;
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
28
lib/types/link.dart
Normal file
28
lib/types/link.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'link.g.dart';
|
||||||
|
part 'link.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnLinkMeta with _$SnLinkMeta {
|
||||||
|
const SnLinkMeta._();
|
||||||
|
|
||||||
|
const factory SnLinkMeta({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String entryId,
|
||||||
|
required String? icon,
|
||||||
|
required String url,
|
||||||
|
required String? title,
|
||||||
|
required String? image,
|
||||||
|
required String? video,
|
||||||
|
required String? audio,
|
||||||
|
required String? description,
|
||||||
|
required String? siteName,
|
||||||
|
required String? type,
|
||||||
|
}) = _SnLinkMeta;
|
||||||
|
|
||||||
|
factory SnLinkMeta.fromJson(Map<String, dynamic> json) => _$SnLinkMetaFromJson(json);
|
||||||
|
}
|
450
lib/types/link.freezed.dart
Normal file
450
lib/types/link.freezed.dart
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'link.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
SnLinkMeta _$SnLinkMetaFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnLinkMeta.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnLinkMeta {
|
||||||
|
int get id => throw _privateConstructorUsedError;
|
||||||
|
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||||
|
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||||
|
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||||
|
String get entryId => throw _privateConstructorUsedError;
|
||||||
|
String? get icon => throw _privateConstructorUsedError;
|
||||||
|
String get url => throw _privateConstructorUsedError;
|
||||||
|
String? get title => throw _privateConstructorUsedError;
|
||||||
|
String? get image => throw _privateConstructorUsedError;
|
||||||
|
String? get video => throw _privateConstructorUsedError;
|
||||||
|
String? get audio => throw _privateConstructorUsedError;
|
||||||
|
String? get description => throw _privateConstructorUsedError;
|
||||||
|
String? get siteName => throw _privateConstructorUsedError;
|
||||||
|
String? get type => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnLinkMeta to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnLinkMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnLinkMetaCopyWith<SnLinkMeta> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnLinkMetaCopyWith<$Res> {
|
||||||
|
factory $SnLinkMetaCopyWith(
|
||||||
|
SnLinkMeta value, $Res Function(SnLinkMeta) then) =
|
||||||
|
_$SnLinkMetaCopyWithImpl<$Res, SnLinkMeta>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int id,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
DateTime? deletedAt,
|
||||||
|
String entryId,
|
||||||
|
String? icon,
|
||||||
|
String url,
|
||||||
|
String? title,
|
||||||
|
String? image,
|
||||||
|
String? video,
|
||||||
|
String? audio,
|
||||||
|
String? description,
|
||||||
|
String? siteName,
|
||||||
|
String? type});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnLinkMetaCopyWithImpl<$Res, $Val extends SnLinkMeta>
|
||||||
|
implements $SnLinkMetaCopyWith<$Res> {
|
||||||
|
_$SnLinkMetaCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnLinkMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? createdAt = null,
|
||||||
|
Object? updatedAt = null,
|
||||||
|
Object? deletedAt = freezed,
|
||||||
|
Object? entryId = null,
|
||||||
|
Object? icon = freezed,
|
||||||
|
Object? url = null,
|
||||||
|
Object? title = freezed,
|
||||||
|
Object? image = freezed,
|
||||||
|
Object? video = freezed,
|
||||||
|
Object? audio = freezed,
|
||||||
|
Object? description = freezed,
|
||||||
|
Object? siteName = freezed,
|
||||||
|
Object? type = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
createdAt: null == createdAt
|
||||||
|
? _value.createdAt
|
||||||
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _value.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _value.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
entryId: null == entryId
|
||||||
|
? _value.entryId
|
||||||
|
: entryId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
icon: freezed == icon
|
||||||
|
? _value.icon
|
||||||
|
: icon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
url: null == url
|
||||||
|
? _value.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
title: freezed == title
|
||||||
|
? _value.title
|
||||||
|
: title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
image: freezed == image
|
||||||
|
? _value.image
|
||||||
|
: image // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
video: freezed == video
|
||||||
|
? _value.video
|
||||||
|
: video // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
audio: freezed == audio
|
||||||
|
? _value.audio
|
||||||
|
: audio // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
description: freezed == description
|
||||||
|
? _value.description
|
||||||
|
: description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
siteName: freezed == siteName
|
||||||
|
? _value.siteName
|
||||||
|
: siteName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
type: freezed == type
|
||||||
|
? _value.type
|
||||||
|
: type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnLinkMetaImplCopyWith<$Res>
|
||||||
|
implements $SnLinkMetaCopyWith<$Res> {
|
||||||
|
factory _$$SnLinkMetaImplCopyWith(
|
||||||
|
_$SnLinkMetaImpl value, $Res Function(_$SnLinkMetaImpl) then) =
|
||||||
|
__$$SnLinkMetaImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int id,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
DateTime? deletedAt,
|
||||||
|
String entryId,
|
||||||
|
String? icon,
|
||||||
|
String url,
|
||||||
|
String? title,
|
||||||
|
String? image,
|
||||||
|
String? video,
|
||||||
|
String? audio,
|
||||||
|
String? description,
|
||||||
|
String? siteName,
|
||||||
|
String? type});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnLinkMetaImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnLinkMetaCopyWithImpl<$Res, _$SnLinkMetaImpl>
|
||||||
|
implements _$$SnLinkMetaImplCopyWith<$Res> {
|
||||||
|
__$$SnLinkMetaImplCopyWithImpl(
|
||||||
|
_$SnLinkMetaImpl _value, $Res Function(_$SnLinkMetaImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnLinkMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? createdAt = null,
|
||||||
|
Object? updatedAt = null,
|
||||||
|
Object? deletedAt = freezed,
|
||||||
|
Object? entryId = null,
|
||||||
|
Object? icon = freezed,
|
||||||
|
Object? url = null,
|
||||||
|
Object? title = freezed,
|
||||||
|
Object? image = freezed,
|
||||||
|
Object? video = freezed,
|
||||||
|
Object? audio = freezed,
|
||||||
|
Object? description = freezed,
|
||||||
|
Object? siteName = freezed,
|
||||||
|
Object? type = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnLinkMetaImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
createdAt: null == createdAt
|
||||||
|
? _value.createdAt
|
||||||
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _value.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _value.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
entryId: null == entryId
|
||||||
|
? _value.entryId
|
||||||
|
: entryId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
icon: freezed == icon
|
||||||
|
? _value.icon
|
||||||
|
: icon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
url: null == url
|
||||||
|
? _value.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
title: freezed == title
|
||||||
|
? _value.title
|
||||||
|
: title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
image: freezed == image
|
||||||
|
? _value.image
|
||||||
|
: image // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
video: freezed == video
|
||||||
|
? _value.video
|
||||||
|
: video // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
audio: freezed == audio
|
||||||
|
? _value.audio
|
||||||
|
: audio // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
description: freezed == description
|
||||||
|
? _value.description
|
||||||
|
: description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
siteName: freezed == siteName
|
||||||
|
? _value.siteName
|
||||||
|
: siteName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
type: freezed == type
|
||||||
|
? _value.type
|
||||||
|
: type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SnLinkMetaImpl extends _SnLinkMeta {
|
||||||
|
const _$SnLinkMetaImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.entryId,
|
||||||
|
required this.icon,
|
||||||
|
required this.url,
|
||||||
|
required this.title,
|
||||||
|
required this.image,
|
||||||
|
required this.video,
|
||||||
|
required this.audio,
|
||||||
|
required this.description,
|
||||||
|
required this.siteName,
|
||||||
|
required this.type})
|
||||||
|
: super._();
|
||||||
|
|
||||||
|
factory _$SnLinkMetaImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnLinkMetaImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int id;
|
||||||
|
@override
|
||||||
|
final DateTime createdAt;
|
||||||
|
@override
|
||||||
|
final DateTime updatedAt;
|
||||||
|
@override
|
||||||
|
final DateTime? deletedAt;
|
||||||
|
@override
|
||||||
|
final String entryId;
|
||||||
|
@override
|
||||||
|
final String? icon;
|
||||||
|
@override
|
||||||
|
final String url;
|
||||||
|
@override
|
||||||
|
final String? title;
|
||||||
|
@override
|
||||||
|
final String? image;
|
||||||
|
@override
|
||||||
|
final String? video;
|
||||||
|
@override
|
||||||
|
final String? audio;
|
||||||
|
@override
|
||||||
|
final String? description;
|
||||||
|
@override
|
||||||
|
final String? siteName;
|
||||||
|
@override
|
||||||
|
final String? type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnLinkMeta(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, entryId: $entryId, icon: $icon, url: $url, title: $title, image: $image, video: $video, audio: $audio, description: $description, siteName: $siteName, type: $type)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnLinkMetaImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.createdAt, createdAt) ||
|
||||||
|
other.createdAt == createdAt) &&
|
||||||
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
|
other.updatedAt == updatedAt) &&
|
||||||
|
(identical(other.deletedAt, deletedAt) ||
|
||||||
|
other.deletedAt == deletedAt) &&
|
||||||
|
(identical(other.entryId, entryId) || other.entryId == entryId) &&
|
||||||
|
(identical(other.icon, icon) || other.icon == icon) &&
|
||||||
|
(identical(other.url, url) || other.url == url) &&
|
||||||
|
(identical(other.title, title) || other.title == title) &&
|
||||||
|
(identical(other.image, image) || other.image == image) &&
|
||||||
|
(identical(other.video, video) || other.video == video) &&
|
||||||
|
(identical(other.audio, audio) || other.audio == audio) &&
|
||||||
|
(identical(other.description, description) ||
|
||||||
|
other.description == description) &&
|
||||||
|
(identical(other.siteName, siteName) ||
|
||||||
|
other.siteName == siteName) &&
|
||||||
|
(identical(other.type, type) || other.type == type));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
deletedAt,
|
||||||
|
entryId,
|
||||||
|
icon,
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
image,
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
description,
|
||||||
|
siteName,
|
||||||
|
type);
|
||||||
|
|
||||||
|
/// Create a copy of SnLinkMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnLinkMetaImplCopyWith<_$SnLinkMetaImpl> get copyWith =>
|
||||||
|
__$$SnLinkMetaImplCopyWithImpl<_$SnLinkMetaImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnLinkMetaImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnLinkMeta extends SnLinkMeta {
|
||||||
|
const factory _SnLinkMeta(
|
||||||
|
{required final int id,
|
||||||
|
required final DateTime createdAt,
|
||||||
|
required final DateTime updatedAt,
|
||||||
|
required final DateTime? deletedAt,
|
||||||
|
required final String entryId,
|
||||||
|
required final String? icon,
|
||||||
|
required final String url,
|
||||||
|
required final String? title,
|
||||||
|
required final String? image,
|
||||||
|
required final String? video,
|
||||||
|
required final String? audio,
|
||||||
|
required final String? description,
|
||||||
|
required final String? siteName,
|
||||||
|
required final String? type}) = _$SnLinkMetaImpl;
|
||||||
|
const _SnLinkMeta._() : super._();
|
||||||
|
|
||||||
|
factory _SnLinkMeta.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SnLinkMetaImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get id;
|
||||||
|
@override
|
||||||
|
DateTime get createdAt;
|
||||||
|
@override
|
||||||
|
DateTime get updatedAt;
|
||||||
|
@override
|
||||||
|
DateTime? get deletedAt;
|
||||||
|
@override
|
||||||
|
String get entryId;
|
||||||
|
@override
|
||||||
|
String? get icon;
|
||||||
|
@override
|
||||||
|
String get url;
|
||||||
|
@override
|
||||||
|
String? get title;
|
||||||
|
@override
|
||||||
|
String? get image;
|
||||||
|
@override
|
||||||
|
String? get video;
|
||||||
|
@override
|
||||||
|
String? get audio;
|
||||||
|
@override
|
||||||
|
String? get description;
|
||||||
|
@override
|
||||||
|
String? get siteName;
|
||||||
|
@override
|
||||||
|
String? get type;
|
||||||
|
|
||||||
|
/// Create a copy of SnLinkMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnLinkMetaImplCopyWith<_$SnLinkMetaImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
45
lib/types/link.g.dart
Normal file
45
lib/types/link.g.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'link.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$SnLinkMetaImpl _$$SnLinkMetaImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnLinkMetaImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
entryId: json['entry_id'] as String,
|
||||||
|
icon: json['icon'] as String?,
|
||||||
|
url: json['url'] as String,
|
||||||
|
title: json['title'] as String?,
|
||||||
|
image: json['image'] as String?,
|
||||||
|
video: json['video'] as String?,
|
||||||
|
audio: json['audio'] as String?,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
siteName: json['site_name'] as String?,
|
||||||
|
type: json['type'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnLinkMetaImplToJson(_$SnLinkMetaImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'entry_id': instance.entryId,
|
||||||
|
'icon': instance.icon,
|
||||||
|
'url': instance.url,
|
||||||
|
'title': instance.title,
|
||||||
|
'image': instance.image,
|
||||||
|
'video': instance.video,
|
||||||
|
'audio': instance.audio,
|
||||||
|
'description': instance.description,
|
||||||
|
'site_name': instance.siteName,
|
||||||
|
'type': instance.type,
|
||||||
|
};
|
@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||||
|
import 'package:surface/widgets/link_preview.dart';
|
||||||
import 'package:surface/widgets/markdown_content.dart';
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
import 'package:swipe_to/swipe_to.dart';
|
import 'package:swipe_to/swipe_to.dart';
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
final Function(SnChatMessage)? onReply;
|
final Function(SnChatMessage)? onReply;
|
||||||
final Function(SnChatMessage)? onEdit;
|
final Function(SnChatMessage)? onEdit;
|
||||||
final Function(SnChatMessage)? onDelete;
|
final Function(SnChatMessage)? onDelete;
|
||||||
|
|
||||||
const ChatMessage({
|
const ChatMessage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
@ -63,7 +65,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
onReply!(data);
|
onReply!(data);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (isOwner && onEdit != null)
|
if (isOwner && data.type == 'messages.new' && onEdit != null)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'edit'.tr(),
|
label: 'edit'.tr(),
|
||||||
icon: Symbols.edit,
|
icon: Symbols.edit,
|
||||||
@ -71,7 +73,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
onEdit!(data);
|
onEdit!(data);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (isOwner && onDelete != null)
|
if (isOwner && data.type == 'messages.new' && onDelete != null)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'delete'.tr(),
|
label: 'delete'.tr(),
|
||||||
icon: Symbols.delete,
|
icon: Symbols.delete,
|
||||||
@ -109,9 +111,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
radius: 12,
|
radius: 12,
|
||||||
).padding(right: 6),
|
).padding(right: 6),
|
||||||
Text(
|
Text(
|
||||||
(data.sender.nick?.isNotEmpty ?? false)
|
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
||||||
? data.sender.nick!
|
|
||||||
: user?.nick ?? 'unknown',
|
|
||||||
).bold(),
|
).bold(),
|
||||||
const Gap(6),
|
const Gap(6),
|
||||||
Text(
|
Text(
|
||||||
@ -123,8 +123,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
if (data.preload?.quoteEvent != null)
|
if (data.preload?.quoteEvent != null)
|
||||||
StyledWidget(Container(
|
StyledWidget(Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius:
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
const BorderRadius.all(Radius.circular(8)),
|
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
@ -153,6 +152,8 @@ class ChatMessage extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
).opacity(isPending ? 0.5 : 1),
|
).opacity(isPending ? 0.5 : 1),
|
||||||
|
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
|
||||||
|
LinkPreviewWidget(text: data.body['text']!),
|
||||||
if (data.preload?.attachments?.isNotEmpty ?? false)
|
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
@ -161,10 +162,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
maxHeight: 520,
|
maxHeight: 520,
|
||||||
listPadding: const EdgeInsets.only(top: 8),
|
listPadding: const EdgeInsets.only(top: 8),
|
||||||
),
|
),
|
||||||
if (!hasMerged && !isCompact)
|
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
|
||||||
const Gap(12)
|
|
||||||
else if (!isCompact)
|
|
||||||
const Gap(6),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -174,6 +172,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
|
|
||||||
class _ChatMessageText extends StatelessWidget {
|
class _ChatMessageText extends StatelessWidget {
|
||||||
final SnChatMessage data;
|
final SnChatMessage data;
|
||||||
|
|
||||||
const _ChatMessageText({super.key, required this.data});
|
const _ChatMessageText({super.key, required this.data});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -184,6 +183,7 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
MarkdownTextContent(
|
MarkdownTextContent(
|
||||||
content: data.body['text'],
|
content: data.body['text'],
|
||||||
|
isSelectable: true,
|
||||||
isAutoWarp: true,
|
isAutoWarp: true,
|
||||||
),
|
),
|
||||||
if (data.updatedAt != data.createdAt)
|
if (data.updatedAt != data.createdAt)
|
||||||
@ -212,6 +212,7 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
|
|
||||||
class _ChatMessageSystemNotify extends StatelessWidget {
|
class _ChatMessageSystemNotify extends StatelessWidget {
|
||||||
final SnChatMessage data;
|
final SnChatMessage data;
|
||||||
|
|
||||||
const _ChatMessageSystemNotify({super.key, required this.data});
|
const _ChatMessageSystemNotify({super.key, required this.data});
|
||||||
|
|
||||||
String _formatDuration(Duration duration) {
|
String _formatDuration(Duration duration) {
|
||||||
|
170
lib/widgets/link_preview.dart
Normal file
170
lib/widgets/link_preview.dart
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:marquee/marquee.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/types/link.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
import '../providers/link_preview.dart';
|
||||||
|
|
||||||
|
class LinkPreviewWidget extends StatefulWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const LinkPreviewWidget({super.key, required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LinkPreviewWidget> createState() => _LinkPreviewWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinkPreviewWidgetState extends State<LinkPreviewWidget> {
|
||||||
|
final List<SnLinkMeta> _links = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _getLinkMeta() async {
|
||||||
|
final linkRegex = RegExp(r'https?:\/\/[^\s/$.?#].[^\s]*');
|
||||||
|
final links = linkRegex.allMatches(widget.text).map((e) => e.group(0)).toSet();
|
||||||
|
|
||||||
|
final lp = context.read<SnLinkPreviewProvider>();
|
||||||
|
|
||||||
|
final List<Future<SnLinkMeta?>> futures = links.where((e) => e != null).map((e) => lp.getLinkMeta(e!)).toList();
|
||||||
|
final results = await Future.wait(futures);
|
||||||
|
|
||||||
|
_links.addAll(results.where((e) => e != null).map((e) => e!).toList());
|
||||||
|
if (_links.isNotEmpty && mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getLinkMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_links.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: _links.map((e) => _LinkPreviewEntry(meta: e)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinkPreviewEntry extends StatelessWidget {
|
||||||
|
final SnLinkMeta meta;
|
||||||
|
|
||||||
|
const _LinkPreviewEntry({
|
||||||
|
super.key,
|
||||||
|
required this.meta,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) ? double.infinity : 480,
|
||||||
|
),
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (meta.image != null)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: ClipRRect(
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
meta.image!,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (meta.icon?.isNotEmpty ?? false)
|
||||||
|
StyledWidget(
|
||||||
|
meta.icon!.endsWith('.svg')
|
||||||
|
? SvgPicture.network(meta.icon!)
|
||||||
|
: UniversalImage(
|
||||||
|
meta.icon!,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
cacheHeight: 36,
|
||||||
|
cacheWidth: 36,
|
||||||
|
),
|
||||||
|
).padding(all: 4, right: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: ((meta.title?.length ?? 0) > 40)
|
||||||
|
? Marquee(
|
||||||
|
text: meta.title ?? 'unknown'.tr(),
|
||||||
|
style: TextStyle(fontSize: 17, height: 1),
|
||||||
|
scrollAxis: Axis.horizontal,
|
||||||
|
showFadingOnlyWhenScrolling: true,
|
||||||
|
pauseAfterRound: const Duration(seconds: 3),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
meta.title ?? 'unknown'.tr(),
|
||||||
|
style: TextStyle(fontSize: 17, height: 1),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (meta.siteName != null)
|
||||||
|
Text(
|
||||||
|
meta.siteName!,
|
||||||
|
style: TextStyle(fontSize: 13, height: 0.9),
|
||||||
|
).fontSize(11),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
),
|
||||||
|
if (meta.description != null)
|
||||||
|
Text(
|
||||||
|
meta.description!,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).padding(horizontal: 16, bottom: 8),
|
||||||
|
Text(
|
||||||
|
meta.url,
|
||||||
|
style: GoogleFonts.roboto(fontSize: 11, height: 0.9),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).opacity(0.75).padding(horizontal: 16),
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
'poweredBy'.tr(args: ['HyperNet.Reader']),
|
||||||
|
style: GoogleFonts.roboto(fontSize: 11, height: 0.9),
|
||||||
|
).opacity(0.75).padding(horizontal: 16),
|
||||||
|
const Gap(16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(meta.url, mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ import 'package:surface/types/reaction.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/link_preview.dart';
|
||||||
import 'package:surface/widgets/markdown_content.dart';
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||||
@ -103,7 +104,7 @@ class PostItem extends StatelessWidget {
|
|||||||
).create();
|
).create();
|
||||||
await imageFile.writeAsBytes(capturedImage);
|
await imageFile.writeAsBytes(capturedImage);
|
||||||
|
|
||||||
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
await Share.shareXFiles(
|
await Share.shareXFiles(
|
||||||
[XFile(imageFile.path)],
|
[XFile(imageFile.path)],
|
||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
@ -132,6 +133,7 @@ class PostItem extends StatelessWidget {
|
|||||||
_PostContentHeader(
|
_PostContentHeader(
|
||||||
data: data,
|
data: data,
|
||||||
isAuthor: isAuthor,
|
isAuthor: isAuthor,
|
||||||
|
isRelativeDate: !showFullPost,
|
||||||
onShare: () => _doShare(context),
|
onShare: () => _doShare(context),
|
||||||
onShareImage: () => _doShareViaPicture(context),
|
onShareImage: () => _doShareViaPicture(context),
|
||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
@ -204,6 +206,7 @@ class PostItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_PostContentHeader(
|
_PostContentHeader(
|
||||||
isAuthor: isAuthor,
|
isAuthor: isAuthor,
|
||||||
|
isRelativeDate: !showFullPost,
|
||||||
data: data,
|
data: data,
|
||||||
showMenu: showMenu,
|
showMenu: showMenu,
|
||||||
onShare: () => _doShare(context),
|
onShare: () => _doShare(context),
|
||||||
@ -217,10 +220,12 @@ class PostItem extends StatelessWidget {
|
|||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article' && showFullPost,
|
isEnlarge: data.type == 'article' && showFullPost,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
_PostContentBody(
|
if (data.body['content']?.isNotEmpty ?? false)
|
||||||
data: data,
|
_PostContentBody(
|
||||||
isEnlarge: data.type == 'article' && showFullPost,
|
data: data,
|
||||||
).padding(horizontal: 16, bottom: 6),
|
isSelectable: showFullPost,
|
||||||
|
isEnlarge: data.type == 'article' && showFullPost,
|
||||||
|
).padding(horizontal: 16, bottom: 6),
|
||||||
if (data.repostTo != null)
|
if (data.repostTo != null)
|
||||||
_PostQuoteContent(child: data.repostTo!).padding(
|
_PostQuoteContent(child: data.repostTo!).padding(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
@ -247,6 +252,10 @@ class PostItem extends StatelessWidget {
|
|||||||
maxHeight: 560,
|
maxHeight: 560,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
|
if (data.body['content'] != null)
|
||||||
|
LinkPreviewWidget(
|
||||||
|
text: data.body['content'],
|
||||||
|
).padding(horizontal: 4),
|
||||||
Container(
|
Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -312,10 +321,11 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article',
|
isEnlarge: data.type == 'article',
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
_PostContentBody(
|
if (data.body['content']?.isNotEmpty ?? false)
|
||||||
data: data,
|
_PostContentBody(
|
||||||
isEnlarge: data.type == 'article',
|
data: data,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
isEnlarge: data.type == 'article',
|
||||||
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.repostTo != null)
|
if (data.repostTo != null)
|
||||||
_PostQuoteContent(
|
_PostQuoteContent(
|
||||||
child: data.repostTo!,
|
child: data.repostTo!,
|
||||||
@ -327,6 +337,10 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
isFlatted: true,
|
isFlatted: true,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
|
if (data.body['content'] != null)
|
||||||
|
LinkPreviewWidget(
|
||||||
|
text: data.body['content'],
|
||||||
|
).padding(horizontal: 4),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -372,7 +386,7 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if(data.body['content_truncated'] == true)
|
if (data.body['content_truncated'] == true)
|
||||||
Text(
|
Text(
|
||||||
'postImageShareReadMore'.tr(),
|
'postImageShareReadMore'.tr(),
|
||||||
style: GoogleFonts.robotoMono(fontSize: 11),
|
style: GoogleFonts.robotoMono(fontSize: 11),
|
||||||
@ -850,16 +864,19 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
class _PostContentBody extends StatelessWidget {
|
class _PostContentBody extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
final bool isEnlarge;
|
final bool isEnlarge;
|
||||||
|
final bool isSelectable;
|
||||||
|
|
||||||
const _PostContentBody({
|
const _PostContentBody({
|
||||||
required this.data,
|
required this.data,
|
||||||
this.isEnlarge = false,
|
this.isEnlarge = false,
|
||||||
|
this.isSelectable = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (data.body['content'] == null) return const SizedBox.shrink();
|
if (data.body['content'] == null) return const SizedBox.shrink();
|
||||||
return MarkdownTextContent(
|
return MarkdownTextContent(
|
||||||
|
isSelectable: isSelectable,
|
||||||
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
||||||
content: data.body['content'],
|
content: data.body['content'],
|
||||||
attachments: data.preload?.attachments,
|
attachments: data.preload?.attachments,
|
||||||
|
56
pubspec.lock
56
pubspec.lock
@ -454,6 +454,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
fading_edge_scrollview:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fading_edge_scrollview
|
||||||
|
sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -704,6 +712,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.7.0"
|
||||||
|
flutter_svg:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_svg
|
||||||
|
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.16"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -1050,6 +1066,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.2.2"
|
version: "7.2.2"
|
||||||
|
marquee:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: marquee
|
||||||
|
sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1226,6 +1250,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1895,6 +1927,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.1"
|
version: "4.5.1"
|
||||||
|
vector_graphics:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics
|
||||||
|
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.15"
|
||||||
|
vector_graphics_codec:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_codec
|
||||||
|
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.12"
|
||||||
|
vector_graphics_compiler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_compiler
|
||||||
|
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.16"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.0.1+24
|
version: 2.0.1+25
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@ -102,6 +102,8 @@ dependencies:
|
|||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
file_saver: ^0.2.14
|
file_saver: ^0.2.14
|
||||||
device_info_plus: ^11.2.0
|
device_info_plus: ^11.2.0
|
||||||
|
marquee: ^2.3.0
|
||||||
|
flutter_svg: ^2.0.16
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user