Compare commits

...

8 Commits

Author SHA1 Message Date
LittleSheep
a3aa694076 🔀 Merge pull request #20 from Texas0295/master
🐛 change -Werror to -Wextra
2025-04-11 00:37:42 +08:00
Texas0295
e98ee562ef 🐛 change -Werror to -Wextra 2025-04-10 20:12:18 +08:00
63ff6df93a 🐛 Fix android build failed 2025-04-07 23:59:38 +08:00
f95eadd3e6 🐛 Fix bugs 2025-04-07 20:56:34 +08:00
9a8e40b288 🚀 Launch 2.5.2+92 2025-04-07 00:53:30 +08:00
cb0986efee 🐛 Bug fixes on live stream post 2025-04-07 00:49:43 +08:00
ce3d19fb7b Live video 2025-04-07 00:01:36 +08:00
935cf774b1 Create attachment with referenced link 2025-04-06 23:03:01 +08:00
18 changed files with 439 additions and 226 deletions

2
android/.gitignore vendored
View File

@@ -11,3 +11,5 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
app/.cxx

View File

@@ -10,10 +10,15 @@ plugins {
}
dependencies {
// implementation('org.jitsi.react:jitsi-meet-sdk:11.1.1') { transitive = true }
// implementation 'com.facebook.fresco:webpsupport:2.6.0'
// implementation 'com.facebook.fresco:animated-webp:2.6.0'
// implementation 'com.facebook.react:react-android:0.75.5'
// implementation 'com.facebook.react:hermes-android:0.75.5'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.glance:glance:1.1.1'
implementation 'androidx.glance:glance-appwidget:1.1.1'
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6'
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.8'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'io.coil-kt.coil3:coil-compose:3.0.4'
@@ -73,8 +78,10 @@ android {
}
release {
signingConfig = signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

96
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,96 @@
-keepclassmembers class kotlin.Metadata { *; }
-keep class dev.solsynth.solian.** { *; }
-keep public class dev.solsynth.solian.data.** { public *; }
-keepclassmembers class dev.solsynth.solian.data.** { *; }
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes EnclosingMethod
-keep class com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-dontwarn com.facebook.imagepipeline.nativecode.WebpTranscoder
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
}
-keep @com.facebook.proguard.annotations.DoNotStripAny class * {
*;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * implements com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * implements com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; }
-keep,includedescriptorclasses class com.facebook.react.turbomodule.core.** { *; }
# hermes
-keep class com.facebook.jni.** { *; }
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# yoga
-keep,allowobfuscation @interface com.facebook.yoga.annotations.DoNotStrip
-keep @com.facebook.yoga.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.yoga.annotations.DoNotStrip *;
}
# WebRTC
-keep class org.webrtc.** { *; }
-dontwarn org.chromium.build.BuildHooksAndroid
# Jisti Meet SDK
-keep class org.jitsi.meet.** { *; }
-keep class org.jitsi.meet.sdk.** { *; }
# We added the following when we switched minifyEnabled on. Probably because we
# ran the app and hit problems...
-keep class com.facebook.react.bridge.CatalystInstanceImpl { *; }
-keep class com.facebook.react.bridge.ExecutorToken { *; }
-keep class com.facebook.react.bridge.JavaScriptExecutor { *; }
-keep class com.facebook.react.bridge.ModuleRegistryHolder { *; }
-keep class com.facebook.react.bridge.ReadableType { *; }
-keep class com.facebook.react.bridge.queue.NativeRunnable { *; }
-keep class com.facebook.react.devsupport.** { *; }
-dontwarn com.facebook.react.devsupport.**
-dontwarn com.google.appengine.**
-dontwarn com.squareup.okhttp.**
-dontwarn javax.servlet.**
# ^^^ We added the above when we switched minifyEnabled on.
# Rule to avoid build errors related to SVGs.
-keep public class com.horcrux.svg.** {*;}
# https://github.com/facebook/fresco/issues/2638
-keep public class com.facebook.imageutils.** {
public *;
}

View File

@@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.INTERNET" />
@@ -118,4 +118,4 @@
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
</manifest>

View File

@@ -1,14 +0,0 @@
-keepclassmembers class kotlin.Metadata { *; }
-keep class dev.solsynth.solian.** { *; }
-keep public class dev.solsynth.solian.data.** { public *; }
-keepclassmembers class dev.solsynth.solian.data.** { *; }
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes EnclosingMethod
-keep class com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

View File

@@ -10,18 +10,22 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
maven {
url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
}
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://www.jitpack.io' }
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.7.3' apply false
id "com.android.application" version '8.9.1' apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.firebase.crashlytics" version "2.8.1" apply false
id "com.google.gms.google-services" version "4.4.2" apply false
id "com.google.firebase.crashlytics" version "3.0.3" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}

View File

@@ -543,6 +543,7 @@
"attachmentSaved": "Saved to album",
"attachmentSavedDesktop": "Saved to Downloads folder",
"openInAlbum": "Open in album",
"openInBrowser": "Open in browser",
"postAbuseReport": "Report Post",
"postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
"abuseReport": "Abuse Report",
@@ -954,5 +955,15 @@
"attachmentEditorUploadHint": "This attachment is uploaded.",
"attachmentRating": "Rating",
"fieldAttachmentRating": "Content Rating",
"fieldAttachmentQuality": "Quality Rating"
"fieldAttachmentQuality": "Quality Rating",
"attachmentReferenceLink": "Use external attachment",
"fieldAttachmentReferenceLink": "Reference Link",
"attachmentReferenceLinkDescription": "It will be used as the source file of the attachment. The link needs to allow cross-origin access.",
"fieldAttachmentMimetype": "Mimetype",
"postVideoLive": "Live Stream",
"postVideoLiveDescription": "This is a live video, you can embed the source site by yourself.",
"postVideoRendererWeb": "WebView Rendering",
"postVideoRendererWebDescription": "Use WebView to render the content",
"fieldPostVideoUrl": "Video URL",
"fieldPostVideoUrlDescription": "The URL of the video content, it can be a webpage, and will be rendered by iFrame / WebView."
}

View File

@@ -541,6 +541,7 @@
"attachmentSaved": "已保存到相册",
"attachmentSavedDesktop": "已保存到下载目录",
"openInAlbum": "在相册中打开",
"openInBrowser": "在浏览器中打开",
"postAbuseReport": "检举帖子",
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
"abuseReport": "检举",
@@ -951,5 +952,15 @@
"attachmentEditorUploadHint": "该附件已上传。",
"attachmentRating": "评级",
"fieldAttachmentRating": "内容分级",
"fieldAttachmentQuality": "质量评分"
"fieldAttachmentQuality": "质量评分",
"attachmentReferenceLink": "引用外部附件",
"fieldAttachmentReferenceLink": "引用连接",
"attachmentReferenceLinkDescription": "作为附件的源文件。需要链接允许跨域访问。",
"fieldAttachmentMimetype": "文件类型",
"postVideoLive": "直播",
"postVideoLiveDescription": "这是一条直播影片,允许用户自行嵌入源站。",
"postVideoRendererWeb": "网页渲染器",
"postVideoRendererWebDescription": "使用 WebView 渲染内容。",
"fieldPostVideoUrl": "视频流地址",
"fieldPostVideoUrlDescription": "视频内容的地址,可以为网页,将会使用 iFrame / WebView 渲染。"
}

View File

@@ -219,6 +219,8 @@ class PostWriteController extends ChangeNotifier {
List<PostWriteMedia> attachments = List.empty(growable: true);
DateTime? publishedAt, publishedUntil;
SnAttachment? videoAttachment;
String videoUrl = '';
bool videoLive = false;
SnPoll? poll;
Future<void> fetchRelatedPost(
@@ -241,9 +243,13 @@ class PostWriteController extends ChangeNotifier {
contentController.text = post.body['content'] ?? '';
aliasController.text = post.alias ?? '';
rewardController.text = post.body['reward']?.toString() ?? '';
videoAttachment = post.body['video'] != null
? SnAttachment.fromJson(post.body['video'])
: null;
if (post.body['video'] != null) {
if (post.body['video'] is String) {
videoUrl = post.body['video'];
} else {
videoAttachment = SnAttachment.fromJson(post.body['video']);
}
}
publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
@@ -262,6 +268,7 @@ class PostWriteController extends ChangeNotifier {
);
poll = post.poll;
videoLive = post.body['is_live'] ?? false;
editingDraft = post.isDraft;
if (post.body['thumbnail'] != null) {
@@ -445,8 +452,9 @@ class PostWriteController extends ChangeNotifier {
titleController.text = data['title'] ?? '';
descriptionController.text = data['description'] ?? '';
rewardController.text = data['reward']?.toString() ?? '';
if (data['thumbnail'] != null)
if (data['thumbnail'] != null) {
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
}
attachments.addAll(data['attachments']
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
.cast<PostWriteMedia>());
@@ -455,10 +463,12 @@ class PostWriteController extends ChangeNotifier {
visibility = data['visibility'];
visibleUsers = List.from(data['visible_users_list'] ?? []);
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
if (data['published_at'] != null)
if (data['published_at'] != null) {
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
if (data['published_until'] != null)
}
if (data['published_until'] != null) {
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
}
replyingPost =
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
repostingPost =
@@ -595,9 +605,11 @@ class PostWriteController extends ChangeNotifier {
if (replyingPost != null) 'reply_to': replyingPost!.id,
if (repostingPost != null) 'repost_to': repostingPost!.id,
if (reward != null) 'reward': reward,
if (videoAttachment != null) 'video': videoAttachment!.rid,
if (videoAttachment != null || videoUrl.isNotEmpty)
'video': videoUrl.isNotEmpty ? videoUrl : videoAttachment!.rid,
if (poll != null) 'poll': poll!.id,
if (realm != null) 'realm': realm!.id,
if (videoLive) 'is_live': videoLive,
'is_draft': saveAsDraft,
},
onSendProgress: (count, total) {
@@ -738,6 +750,16 @@ class PostWriteController extends ChangeNotifier {
notifyListeners();
}
void setVideoUrl(String value) {
videoUrl = value;
notifyListeners();
}
void setVideoLive(bool value) {
videoLive = value;
notifyListeners();
}
void setPoll(SnPoll? value) {
poll = value;
notifyListeners();

View File

@@ -120,6 +120,25 @@ class SnAttachmentProvider {
'webp': 'image/webp',
};
Future<SnAttachment> createWithReferenceLink(
String url,
String pool,
Map<String, dynamic>? metadata, {
String? mimetype,
}) async {
final resp = await _sn.client.post(
'/cgi/uc/attachments/referenced',
data: {
'url': url,
'pool': pool,
'metadata': metadata,
if (mimetype != null) 'mimetype': mimetype,
},
);
return SnAttachment.fromJson(resp.data);
}
Future<SnAttachment> directUploadOne(
Uint8List data,
String filename,

View File

@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
@@ -16,7 +15,6 @@ import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/types/attachment.dart';
@@ -25,8 +23,7 @@ import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
import 'package:surface/widgets/attachment/pending_attachment_actions.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
@@ -1108,7 +1105,7 @@ class _PostQuestionEditor extends StatelessWidget {
}
}
class _PostVideoEditor extends StatelessWidget {
class _PostVideoEditor extends StatefulWidget {
final PostWriteController controller;
final Function? onTapPublisher;
final Function? onTapRealm;
@@ -1116,7 +1113,14 @@ class _PostVideoEditor extends StatelessWidget {
const _PostVideoEditor(
{required this.controller, this.onTapPublisher, this.onTapRealm});
void _selectVideo(BuildContext context) async {
@override
State<_PostVideoEditor> createState() => _PostVideoEditorState();
}
class _PostVideoEditorState extends State<_PostVideoEditor> {
final TextEditingController _streamUrlController = TextEditingController();
void _selectVideo() async {
final video = await showDialog<SnAttachment?>(
context: context,
builder: (context) => AttachmentInputDialog(
@@ -1127,78 +1131,25 @@ class _PostVideoEditor extends StatelessWidget {
);
if (!context.mounted) return;
if (video == null) return;
controller.setVideoAttachment(video);
widget.controller.setVideoAttachment(video);
}
void _setAlt(BuildContext context) async {
if (controller.videoAttachment == null) return;
final result = await showDialog<SnAttachment?>(
context: context,
builder: (context) => PendingAttachmentAltDialog(
media: PostWriteMedia(controller.videoAttachment)),
);
if (result == null) return;
controller.setVideoAttachment(result);
@override
void initState() {
_streamUrlController.addListener(() {
if (_streamUrlController.text.isEmpty) {
widget.controller.setVideoUrl('');
} else {
widget.controller.setVideoUrl(_streamUrlController.text);
}
});
super.initState();
}
Future<void> _createBoost(BuildContext context) async {
if (controller.videoAttachment == null) return;
final result = await showDialog<SnAttachmentBoost?>(
context: context,
builder: (context) => PendingAttachmentBoostDialog(
media: PostWriteMedia(controller.videoAttachment)),
);
if (result == null) return;
final newAttach = controller.videoAttachment!.copyWith(
boosts: [...controller.videoAttachment!.boosts, result],
);
controller.setVideoAttachment(newAttach);
}
void _setThumbnail(BuildContext context) async {
if (controller.videoAttachment == null) return;
final thumbnail = await showDialog<SnAttachment?>(
context: context,
builder: (context) => AttachmentInputDialog(
title: 'attachmentSetThumbnail'.tr(),
pool: 'interactive',
analyzeNow: true,
),
);
if (thumbnail == null) return;
if (!context.mounted) return;
try {
final attach = context.read<SnAttachmentProvider>();
final newAttach = await attach.updateOne(
controller.videoAttachment!,
thumbnailId: thumbnail.id,
);
controller.setVideoAttachment(newAttach);
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
}
}
Future<void> _deleteAttachment(BuildContext context) async {
if (controller.videoAttachment == null) return;
try {
final sn = context.read<SnNetworkProvider>();
await sn.client
.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
controller.setVideoAttachment(null);
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
}
@override
void dispose() {
_streamUrlController.dispose();
super.dispose();
}
@override
@@ -1216,10 +1167,10 @@ class _PostVideoEditor extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapPublisher?.call();
widget.onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
content: widget.controller.publisher?.avatar,
),
),
),
@@ -1229,10 +1180,10 @@ class _PostVideoEditor extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
widget.onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
content: widget.controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
@@ -1245,7 +1196,7 @@ class _PostVideoEditor extends StatelessWidget {
children: [
const Gap(6),
TextField(
controller: controller.titleController,
controller: widget.controller.titleController,
decoration: InputDecoration.collapsed(
hintText: 'fieldPostTitle'.tr(),
border: InputBorder.none,
@@ -1256,7 +1207,7 @@ class _PostVideoEditor extends StatelessWidget {
).padding(horizontal: 16),
const Gap(8),
TextField(
controller: controller.descriptionController,
controller: widget.controller.descriptionController,
decoration: InputDecoration.collapsed(
hintText: 'fieldPostDescription'.tr(),
border: InputBorder.none,
@@ -1268,66 +1219,52 @@ class _PostVideoEditor extends StatelessWidget {
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16),
const Gap(12),
Container(
margin: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerColor),
),
child: ContextMenuRegion(
contextMenu: ContextMenu(
entries: [
MenuItem(
label: 'attachmentSetAlt'.tr(),
icon: Symbols.description,
onSelected: () {
_setAlt(context);
},
),
MenuItem(
label: 'attachmentBoost'.tr(),
icon: Symbols.bolt,
onSelected: () {
_createBoost(context);
},
),
MenuItem(
label: 'attachmentSetThumbnail'.tr(),
icon: Symbols.image,
onSelected: () {
_setThumbnail(context);
},
),
MenuItem(
label: 'attachmentCopyRandomId'.tr(),
icon: Symbols.content_copy,
onSelected: () {
Clipboard.setData(ClipboardData(
text: controller.videoAttachment!.rid));
},
),
MenuItem(
label: 'delete'.tr(),
icon: Symbols.delete,
onSelected: () => _deleteAttachment(context),
),
MenuItem(
label: 'unlink'.tr(),
icon: Symbols.link_off,
onSelected: () {
controller.setVideoAttachment(null);
},
),
],
if (widget.controller.videoLive ||
widget.controller.videoAttachment == null)
TextField(
controller: _streamUrlController,
decoration: InputDecoration(
labelText: 'fieldPostVideoUrl'.tr(),
helperText: 'fieldPostVideoUrlDescription'.tr(),
border: OutlineInputBorder(),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16, bottom: 12, top: 2),
if (!widget.controller.videoLive &&
_streamUrlController.text.isEmpty)
Container(
margin: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerColor),
),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: controller.videoAttachment == null
? () => _selectVideo(context)
: null,
onTap: widget.controller.videoAttachment == null
? () => _selectVideo()
: () {
showModalBottomSheet(
context: context,
builder: (context) =>
PendingAttachmentActionSheet(
media: PostWriteMedia(
widget.controller.videoAttachment!,
),
),
).then((value) async {
if (value is PostWriteMedia) {
widget.controller
.setVideoAttachment(value.attachment);
} else if (value == false) {
widget.controller.setVideoAttachment(null);
}
});
},
child: AspectRatio(
aspectRatio: 16 / 9,
child: controller.videoAttachment == null
child: widget.controller.videoAttachment == null
? Center(
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -1343,13 +1280,21 @@ class _PostVideoEditor extends StatelessWidget {
: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AttachmentItem(
data: controller.videoAttachment!,
data: widget.controller.videoAttachment!,
heroTag: const Uuid().v4(),
),
),
),
),
),
const Gap(8),
CheckboxListTile(
secondary: const Icon(Symbols.live_tv),
title: Text('postVideoLive').tr(),
subtitle: Text('postVideoLiveDescription').tr(),
value: widget.controller.videoLive,
onChanged: (value) =>
widget.controller.setVideoLive(value ?? false),
),
],
),

View File

@@ -12,6 +12,9 @@ import 'package:surface/widgets/dialog.dart';
class AttachmentInputDialog extends StatefulWidget {
final String? title;
final bool? analyzeNow;
final bool canPickMedia;
final bool canReferenceLink;
final bool canRandomId;
final SnMediaType? mediaType;
final String pool;
@@ -21,6 +24,9 @@ class AttachmentInputDialog extends StatefulWidget {
required this.pool,
this.analyzeNow = false,
this.mediaType = SnMediaType.image,
this.canPickMedia = true,
this.canReferenceLink = true,
this.canRandomId = true,
});
@override
@@ -29,6 +35,8 @@ class AttachmentInputDialog extends StatefulWidget {
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
final _randomIdController = TextEditingController();
final _referenceLinkController = TextEditingController();
final _referenceMimetypeController = TextEditingController();
XFile? _file;
double? _progress;
@@ -61,6 +69,22 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
if (!mounted) return;
context.showErrorDialog(err);
}
} else if (_referenceLinkController.text.isNotEmpty) {
try {
final attachment = await attach.createWithReferenceLink(
_referenceLinkController.text,
widget.pool,
null,
mimetype: _referenceMimetypeController.text.isNotEmpty
? _referenceMimetypeController.text
: null,
);
if (!mounted) return;
Navigator.pop(context, attachment);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
} else if (_file != null) {
try {
final place = await attach.chunkedUploadInitialize(
@@ -90,44 +114,98 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
return AlertDialog(
title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
content: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('attachmentInputUseRandomId').tr().fontSize(14),
const Gap(8),
TextField(
controller: _randomIdController,
decoration: InputDecoration(
labelText: 'fieldAttachmentRandomId'.tr(),
border: const UnderlineInputBorder(),
isDense: true,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(24),
Text('attachmentInputNew').tr().fontSize(14),
Card(
child: Column(
if (_file == null &&
_referenceLinkController.text.isEmpty &&
widget.canRandomId)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
Text('attachmentInputUseRandomId').tr().fontSize(14),
const Gap(8),
TextField(
controller: _randomIdController,
decoration: InputDecoration(
labelText: 'fieldAttachmentRandomId'.tr(),
border: const OutlineInputBorder(),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
if (_file == null &&
_referenceLinkController.text.isEmpty &&
widget.canReferenceLink)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachmentReferenceLink').tr().fontSize(14),
const Gap(8),
TextField(
controller: _referenceLinkController,
decoration: InputDecoration(
labelText: 'fieldAttachmentReferenceLink'.tr(),
helperText: 'attachmentReferenceLinkDescription'.tr(),
helperMaxLines: 3,
border: const OutlineInputBorder(),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(8),
TextField(
controller: _referenceLinkController,
decoration: InputDecoration(
labelText: 'fieldAttachmentMimetype'.tr(),
helperText: 'class/type',
border: const OutlineInputBorder(),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
if (_referenceLinkController.text.isEmpty &&
_randomIdController.text.isEmpty &&
widget.canPickMedia)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachmentInputNew').tr().fontSize(14),
Card(
margin: EdgeInsets.only(top: 8),
child: Column(
children: [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
leading: const Icon(Symbols.add_photo_alternate),
trailing: const Icon(Symbols.chevron_right),
title: Text('addAttachmentFromAlbum').tr(),
subtitle: _file == null
? Text('unset').tr()
: Text('waitingForUpload').tr(),
onTap: () {
_pickMedia();
},
),
],
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: const Icon(Symbols.add_photo_alternate),
trailing: const Icon(Symbols.chevron_right),
title: Text('addAttachmentFromAlbum').tr(),
subtitle: _file == null
? Text('unset').tr()
: Text('waitingForUpload').tr(),
onTap: () {
_pickMedia();
},
),
],
),
),
if (_isBusy)
LinearProgressIndicator(
value: _progress,

View File

@@ -27,7 +27,12 @@ import 'package:surface/widgets/loading_indicator.dart';
class PendingAttachmentActionSheet extends StatefulWidget {
final PostWriteMedia media;
const PendingAttachmentActionSheet({super.key, required this.media});
final bool canInsertLink;
const PendingAttachmentActionSheet({
super.key,
required this.media,
this.canInsertLink = true,
});
@override
State<PendingAttachmentActionSheet> createState() =>
@@ -270,15 +275,16 @@ class _PendingAttachmentActionSheetState
Navigator.pop(context);
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.add_link),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('attachmentInsertLink').tr(),
onTap: () {
Navigator.pop(context, 'link');
},
),
if (widget.canInsertLink)
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.add_link),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('attachmentInsertLink').tr(),
onTap: () {
Navigator.pop(context, 'link');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.bolt),

View File

@@ -232,7 +232,7 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
final alias = match[0]!;
final anchor = markdown.Element.text('a', alias)
..attributes['href'] = Uri.encodeFull(
'solink://account/${alias.substring(1)}',
'solink://accounts/${alias.substring(1)}',
);
parser.addNode(anchor);

View File

@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -41,6 +42,7 @@ import 'package:surface/widgets/post/post_poll.dart';
import 'package:surface/widgets/post/post_reaction.dart';
import 'package:surface/widgets/post/publisher_popover.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:xml/xml.dart';
class OpenablePostItem extends StatelessWidget {
@@ -2321,24 +2323,48 @@ class _PostVideoPlayer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AttachmentItem(
data: data.body['video'],
heroTag: 'post-video-${data.id}',
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: (data.body['video'] is String)
? InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri(data.body['video']),
),
)
: AttachmentItem(
data: data.body['video'],
heroTag: 'post-video-${data.id}',
),
),
),
),
),
if (data.body['video'] is String)
InkWell(
child: Row(
children: [
const Icon(Symbols.launch, size: 16),
const Gap(6),
Text('openInBrowser').tr(),
],
).opacity(0.8),
onTap: () {
launchUrlString(data.body['video']);
},
).padding(top: 4),
],
);
}
}

View File

@@ -41,7 +41,7 @@ endif()
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE -Wall -Wextra)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()

View File

@@ -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
# 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.
version: 2.5.2+91
version: 2.5.2+92
environment:
sdk: ^3.5.4