Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
088cb4d5a2 | |||
33e84805d7 | |||
9aca6eb674 | |||
e431a54a89 |
83
.github/workflows/build.yml
vendored
Normal file
83
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
name: Build Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-web:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
cache: true
|
||||||
|
- run: flutter pub get
|
||||||
|
- run: flutter build web --release
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-output-web
|
||||||
|
path: build/web
|
||||||
|
build-exe:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
cache: true
|
||||||
|
- run: flutter pub get
|
||||||
|
- run: flutter build windows
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-output-windows
|
||||||
|
path: build/windows/x64/runner/Release
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||||
|
sudo apt-get install -y libmpv-dev mpv
|
||||||
|
sudo apt-get install -y libayatana-appindicator3-dev
|
||||||
|
sudo apt-get install -y keybinder-3.0
|
||||||
|
sudo apt-get install -y libnotify-dev
|
||||||
|
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
|
sudo apt-get install -y gstreamer-1.0
|
||||||
|
- run: flutter pub get
|
||||||
|
- run: flutter build linux
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-output-linux
|
||||||
|
path: build/linux/x64/release/bundle
|
||||||
|
- name: Build AppImage
|
||||||
|
run: |
|
||||||
|
rm -r Solian.AppDir | true
|
||||||
|
mkdir Solian.AppDir
|
||||||
|
cp -r build/linux/x64/release/bundle/* Solian.AppDir
|
||||||
|
cp -r buildtools/appimage_config/* Solian.AppDir
|
||||||
|
cp assets/icon/icon-light-radius.png Solian.AppDir
|
||||||
|
sudo chmod +x buildtools/appimagetool-x86_64.AppImage
|
||||||
|
sudo chmod +x Solian.AppDir/AppRun
|
||||||
|
./buildtools/appimagetool-x86_64.AppImage Solian.AppDir
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-output-linux-appimage
|
||||||
|
path: './*.AppImage*'
|
@ -99,6 +99,16 @@
|
|||||||
"permissionMember": "Member",
|
"permissionMember": "Member",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"forward": "Forward",
|
"forward": "Forward",
|
||||||
|
"repliedTo": "Replied to",
|
||||||
|
"forwarded": "Forwarded",
|
||||||
|
"hasAttachments": {
|
||||||
|
"one": "{} attachment",
|
||||||
|
"other": "{} attachments"
|
||||||
|
},
|
||||||
|
"postHasAttachments": {
|
||||||
|
"one": "{} attachment",
|
||||||
|
"other": "{} attachments"
|
||||||
|
},
|
||||||
"edited": "Edited",
|
"edited": "Edited",
|
||||||
"addVideo": "Add video",
|
"addVideo": "Add video",
|
||||||
"addPhoto": "Add photo",
|
"addPhoto": "Add photo",
|
||||||
@ -315,5 +325,10 @@
|
|||||||
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
|
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
|
||||||
"unauthorized": "Unauthorized",
|
"unauthorized": "Unauthorized",
|
||||||
"unauthorizedHint": "You're not signed in or session expired, please sign in again.",
|
"unauthorizedHint": "You're not signed in or session expired, please sign in again.",
|
||||||
"publisherVisitAccountPage": "Visit the profile of {}"
|
"publisherVisitAccountPage": "Visit the profile of {}",
|
||||||
|
"postVisibility": "Visibility",
|
||||||
|
"postVisibilityPublic": "Public",
|
||||||
|
"postVisibilityFriends": "Friends Only",
|
||||||
|
"postVisibilityUnlisted": "Unlisted",
|
||||||
|
"postVisibilityPrivate": "Private"
|
||||||
}
|
}
|
||||||
|
@ -99,6 +99,14 @@
|
|||||||
"permissionMember": "成员",
|
"permissionMember": "成员",
|
||||||
"reply": "回复",
|
"reply": "回复",
|
||||||
"forward": "转发",
|
"forward": "转发",
|
||||||
|
"repliedTo": "回复了",
|
||||||
|
"forwarded": "转发了",
|
||||||
|
"hasAttachments": {
|
||||||
|
"other": "{}个附件"
|
||||||
|
},
|
||||||
|
"postHasAttachments": {
|
||||||
|
"other": "{}个附件"
|
||||||
|
},
|
||||||
"edited": "已编辑",
|
"edited": "已编辑",
|
||||||
"addVideo": "添加视频",
|
"addVideo": "添加视频",
|
||||||
"addPhoto": "添加照片",
|
"addPhoto": "添加照片",
|
||||||
@ -278,5 +286,10 @@
|
|||||||
"settingsHideBottomNav": "隐藏底部导航",
|
"settingsHideBottomNav": "隐藏底部导航",
|
||||||
"settingsSoundEffects": "音效",
|
"settingsSoundEffects": "音效",
|
||||||
"settingsAprilFoolFeatures": "愚人节功能",
|
"settingsAprilFoolFeatures": "愚人节功能",
|
||||||
"settingsEnterToSend": "按下 Enter 发送"
|
"settingsEnterToSend": "按下 Enter 发送",
|
||||||
|
"postVisibility": "可见性",
|
||||||
|
"postVisibilityPublic": "公开",
|
||||||
|
"postVisibilityFriends": "仅好友可见",
|
||||||
|
"postVisibilityUnlisted": "不公开",
|
||||||
|
"postVisibilityPrivate": "私密"
|
||||||
}
|
}
|
@ -99,6 +99,14 @@
|
|||||||
"permissionMember": "成員",
|
"permissionMember": "成員",
|
||||||
"reply": "回覆",
|
"reply": "回覆",
|
||||||
"forward": "轉發",
|
"forward": "轉發",
|
||||||
|
"repliedTo": "回覆了",
|
||||||
|
"forwarded": "轉發了",
|
||||||
|
"hasAttachments": {
|
||||||
|
"other": "{}個附件"
|
||||||
|
},
|
||||||
|
"postHasAttachments": {
|
||||||
|
"other": "{}個附件"
|
||||||
|
},
|
||||||
"edited": "已編輯",
|
"edited": "已編輯",
|
||||||
"addVideo": "新增影片",
|
"addVideo": "新增影片",
|
||||||
"addPhoto": "新增照片",
|
"addPhoto": "新增照片",
|
||||||
@ -278,5 +286,10 @@
|
|||||||
"settingsHideBottomNav": "隱藏底部導航",
|
"settingsHideBottomNav": "隱藏底部導航",
|
||||||
"settingsSoundEffects": "音效",
|
"settingsSoundEffects": "音效",
|
||||||
"settingsAprilFoolFeatures": "愚人節功能",
|
"settingsAprilFoolFeatures": "愚人節功能",
|
||||||
"settingsEnterToSend": "按下 Enter 傳送"
|
"settingsEnterToSend": "按下 Enter 傳送",
|
||||||
|
"postVisibility": "可見性",
|
||||||
|
"postVisibilityPublic": "公開",
|
||||||
|
"postVisibilityFriends": "僅好友可見",
|
||||||
|
"postVisibilityUnlisted": "不公開",
|
||||||
|
"postVisibilityPrivate": "私密"
|
||||||
}
|
}
|
@ -113,6 +113,7 @@
|
|||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
CloudFile.swift,
|
CloudFile.swift,
|
||||||
|
DataExchange.swift,
|
||||||
);
|
);
|
||||||
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
|
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ import UIKit
|
|||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
@ -66,5 +66,10 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>INStartCallIntent</string>
|
||||||
|
<string>INSendMessageIntent</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -16,17 +16,12 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var token: String = ""
|
var token: String? = UserDefaults.standard.getFlutterToken()
|
||||||
if let tokenJson = UserDefaults.standard.string(forKey: "dyn_user_tk"),
|
if token == nil {
|
||||||
let tokenData = tokenJson.data(using: String.Encoding.utf8),
|
|
||||||
let tokenDict = try? JSONSerialization.jsonObject(with: tokenData) as? [String: Any],
|
|
||||||
let tokenValue = tokenDict["token"] as? String {
|
|
||||||
token = tokenValue
|
|
||||||
} else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverUrl = "https://nt.solian.app"
|
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||||
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
|
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
|
||||||
|
|
||||||
let parameters: [String: Any?] = [
|
let parameters: [String: Any?] = [
|
||||||
@ -35,7 +30,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
|
|||||||
]
|
]
|
||||||
|
|
||||||
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders(
|
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders(
|
||||||
[HTTPHeader(name: "Authorization", value: "AtField \(token)")]
|
[HTTPHeader(name: "Authorization", value: "AtField \(token!)")]
|
||||||
))
|
))
|
||||||
.validate()
|
.validate()
|
||||||
.responseString { response in
|
.responseString { response in
|
||||||
|
31
ios/Runner/Services/DataExchange.swift
Normal file
31
ios/Runner/Services/DataExchange.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// DataExchange.swift
|
||||||
|
// Runner
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/6/2.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension UserDefaults {
|
||||||
|
func getFlutterValue<T>(forKey key: String) -> T? {
|
||||||
|
let prefixedKey = "flutter.\(key)"
|
||||||
|
return self.object(forKey: prefixedKey) as? T
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFlutterToken(forKey key: String = "dyn_user_tk") -> String? {
|
||||||
|
let prefixedKey = "flutter.\(key)"
|
||||||
|
guard let jsonString = self.string(forKey: prefixedKey),
|
||||||
|
let data = jsonString.data(using: .utf8),
|
||||||
|
let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
||||||
|
let jsonDict = jsonObject as? [String: Any],
|
||||||
|
let token = jsonDict["token"] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerUrl(forKey key: String = "app_server_url") -> String {
|
||||||
|
return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app"
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,11 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>INStartCallIntent</string>
|
||||||
|
<string>INSendMessageIntent</string>
|
||||||
|
</array>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
@ -1071,10 +1071,17 @@ class PostComposeRoute extends _i27.PageRouteInfo<PostComposeRouteArgs> {
|
|||||||
PostComposeRoute({
|
PostComposeRoute({
|
||||||
_i28.Key? key,
|
_i28.Key? key,
|
||||||
_i30.SnPost? originalPost,
|
_i30.SnPost? originalPost,
|
||||||
|
_i30.SnPost? repliedPost,
|
||||||
|
_i30.SnPost? forwardedPost,
|
||||||
List<_i27.PageRouteInfo>? children,
|
List<_i27.PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
PostComposeRoute.name,
|
PostComposeRoute.name,
|
||||||
args: PostComposeRouteArgs(key: key, originalPost: originalPost),
|
args: PostComposeRouteArgs(
|
||||||
|
key: key,
|
||||||
|
originalPost: originalPost,
|
||||||
|
repliedPost: repliedPost,
|
||||||
|
forwardedPost: forwardedPost,
|
||||||
|
),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1089,32 +1096,50 @@ class PostComposeRoute extends _i27.PageRouteInfo<PostComposeRouteArgs> {
|
|||||||
return _i18.PostComposeScreen(
|
return _i18.PostComposeScreen(
|
||||||
key: args.key,
|
key: args.key,
|
||||||
originalPost: args.originalPost,
|
originalPost: args.originalPost,
|
||||||
|
repliedPost: args.repliedPost,
|
||||||
|
forwardedPost: args.forwardedPost,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PostComposeRouteArgs {
|
class PostComposeRouteArgs {
|
||||||
const PostComposeRouteArgs({this.key, this.originalPost});
|
const PostComposeRouteArgs({
|
||||||
|
this.key,
|
||||||
|
this.originalPost,
|
||||||
|
this.repliedPost,
|
||||||
|
this.forwardedPost,
|
||||||
|
});
|
||||||
|
|
||||||
final _i28.Key? key;
|
final _i28.Key? key;
|
||||||
|
|
||||||
final _i30.SnPost? originalPost;
|
final _i30.SnPost? originalPost;
|
||||||
|
|
||||||
|
final _i30.SnPost? repliedPost;
|
||||||
|
|
||||||
|
final _i30.SnPost? forwardedPost;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost}';
|
return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost, repliedPost: $repliedPost, forwardedPost: $forwardedPost}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
if (other is! PostComposeRouteArgs) return false;
|
if (other is! PostComposeRouteArgs) return false;
|
||||||
return key == other.key && originalPost == other.originalPost;
|
return key == other.key &&
|
||||||
|
originalPost == other.originalPost &&
|
||||||
|
repliedPost == other.repliedPost &&
|
||||||
|
forwardedPost == other.forwardedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => key.hashCode ^ originalPost.hashCode;
|
int get hashCode =>
|
||||||
|
key.hashCode ^
|
||||||
|
originalPost.hashCode ^
|
||||||
|
repliedPost.hashCode ^
|
||||||
|
forwardedPost.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
@ -63,7 +64,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
|||||||
if (token == null) throw ArgumentError('Token is null');
|
if (token == null) throw ArgumentError('Token is null');
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putMediaToCloud(
|
await putMediaToCloud(
|
||||||
fileData: result,
|
fileData: UniversalFile(
|
||||||
|
data: result,
|
||||||
|
type: UniversalFileType.image,
|
||||||
|
),
|
||||||
atk: token,
|
atk: token,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
filename: result.name,
|
filename: result.name,
|
||||||
|
@ -527,7 +527,10 @@ class EditChatScreen extends HookConsumerWidget {
|
|||||||
if (token == null) throw ArgumentError('Token is null');
|
if (token == null) throw ArgumentError('Token is null');
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putMediaToCloud(
|
await putMediaToCloud(
|
||||||
fileData: result,
|
fileData: UniversalFile(
|
||||||
|
data: result,
|
||||||
|
type: UniversalFileType.image,
|
||||||
|
),
|
||||||
atk: token,
|
atk: token,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
filename: result.name,
|
filename: result.name,
|
||||||
|
@ -17,12 +17,12 @@ import 'package:island/pods/database.dart';
|
|||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.gr.dart';
|
import 'package:island/route.gr.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/call_overlay.dart';
|
import 'package:island/widgets/chat/call_overlay.dart';
|
||||||
import 'package:island/widgets/chat/message_item.dart';
|
import 'package:island/widgets/chat/message_item.dart';
|
||||||
|
import 'package:island/widgets/content/attachment_preview.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
@ -514,7 +514,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const Text('Loading...'),
|
loading: () => const Text('Loading...'),
|
||||||
error:
|
error:
|
||||||
(err, __) => ResponseErrorWidget(
|
(err, _) => ResponseErrorWidget(
|
||||||
error: err,
|
error: err,
|
||||||
onRetry: () => messagesNotifier.loadInitial(),
|
onRetry: () => messagesNotifier.loadInitial(),
|
||||||
),
|
),
|
||||||
@ -615,7 +615,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
progress: null,
|
progress: null,
|
||||||
showAvatar: false,
|
showAvatar: false,
|
||||||
),
|
),
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -680,7 +680,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
attachments.value = newAttachments;
|
attachments.value = newAttachments;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
loading: () => const SizedBox.shrink(),
|
loading: () => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -788,7 +788,7 @@ class _ChatInput extends ConsumerWidget {
|
|||||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
separatorBuilder: (_, _) => const Gap(8),
|
||||||
),
|
),
|
||||||
).padding(top: 12),
|
).padding(top: 12),
|
||||||
if (messageReplyingTo != null ||
|
if (messageReplyingTo != null ||
|
||||||
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/models/realm.dart';
|
import 'package:island/models/realm.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
@ -99,7 +100,10 @@ class EditPublisherScreen extends HookConsumerWidget {
|
|||||||
if (token == null) throw ArgumentError('Token is null');
|
if (token == null) throw ArgumentError('Token is null');
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putMediaToCloud(
|
await putMediaToCloud(
|
||||||
fileData: result,
|
fileData: UniversalFile(
|
||||||
|
data: result,
|
||||||
|
type: UniversalFileType.image,
|
||||||
|
),
|
||||||
atk: token,
|
atk: token,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
filename: result.name,
|
filename: result.name,
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@ -21,6 +19,7 @@ import 'package:island/services/responsive.dart';
|
|||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/attachment_preview.dart';
|
||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
import 'package:island/widgets/post/publishers_modal.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
@ -53,7 +52,14 @@ class PostEditScreen extends HookConsumerWidget {
|
|||||||
@RoutePage()
|
@RoutePage()
|
||||||
class PostComposeScreen extends HookConsumerWidget {
|
class PostComposeScreen extends HookConsumerWidget {
|
||||||
final SnPost? originalPost;
|
final SnPost? originalPost;
|
||||||
const PostComposeScreen({super.key, this.originalPost});
|
final SnPost? repliedPost;
|
||||||
|
final SnPost? forwardedPost;
|
||||||
|
const PostComposeScreen({
|
||||||
|
super.key,
|
||||||
|
this.originalPost,
|
||||||
|
this.repliedPost,
|
||||||
|
this.forwardedPost,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@ -90,9 +96,14 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
text: originalPost?.description,
|
text: originalPost?.description,
|
||||||
);
|
);
|
||||||
final contentController = useTextEditingController(
|
final contentController = useTextEditingController(
|
||||||
text: originalPost?.content,
|
text:
|
||||||
|
originalPost?.content ??
|
||||||
|
(forwardedPost != null ? '> ${forwardedPost!.content}\n\n' : null),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add visibility state with default value from original post or 0 (public)
|
||||||
|
final visibility = useState<int>(originalPost?.visibility ?? 0);
|
||||||
|
|
||||||
final submitting = useState(false);
|
final submitting = useState(false);
|
||||||
|
|
||||||
Future<void> pickPhotoMedia() async {
|
Future<void> pickPhotoMedia() async {
|
||||||
@ -188,12 +199,18 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
await client.request(
|
await client.request(
|
||||||
originalPost == null ? '/posts' : '/posts/${originalPost!.id}',
|
originalPost == null ? '/posts' : '/posts/${originalPost!.id}',
|
||||||
data: {
|
data: {
|
||||||
|
'title': titleController.text,
|
||||||
|
'description': descriptionController.text,
|
||||||
'content': contentController.text,
|
'content': contentController.text,
|
||||||
|
'visibility':
|
||||||
|
visibility.value, // Add visibility field to API request
|
||||||
'attachments':
|
'attachments':
|
||||||
attachments.value
|
attachments.value
|
||||||
.where((e) => e.isOnCloud)
|
.where((e) => e.isOnCloud)
|
||||||
.map((e) => e.data.id)
|
.map((e) => e.data.id)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
if (repliedPost != null) 'replied_post_id': repliedPost!.id,
|
||||||
|
if (forwardedPost != null) 'forwarded_post_id': forwardedPost!.id,
|
||||||
},
|
},
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {'X-Pub': currentPublisher.value?.name},
|
headers: {'X-Pub': currentPublisher.value?.name},
|
||||||
@ -210,7 +227,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handlePaste() async {
|
Future<void> handlePaste() async {
|
||||||
final clipboard = await Pasteboard.image;
|
final clipboard = await Pasteboard.image;
|
||||||
if (clipboard == null) return;
|
if (clipboard == null) return;
|
||||||
|
|
||||||
@ -223,14 +240,93 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleKeyPress(RawKeyEvent event) {
|
void handleKeyPress(RawKeyEvent event) {
|
||||||
if (event is! RawKeyDownEvent) return;
|
if (event is! RawKeyDownEvent) return;
|
||||||
|
|
||||||
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||||
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
||||||
|
|
||||||
if (isPaste && isModifierPressed) {
|
if (isPaste && isModifierPressed) {
|
||||||
_handlePaste();
|
handlePaste();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showVisibilityModal() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: Text('postVisibility'.tr()),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Symbols.public),
|
||||||
|
title: Text('postVisibilityPublic'.tr()),
|
||||||
|
onTap: () {
|
||||||
|
visibility.value = 0;
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
selected: visibility.value == 0,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Symbols.group),
|
||||||
|
title: Text('postVisibilityFriends'.tr()),
|
||||||
|
onTap: () {
|
||||||
|
visibility.value = 1;
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
selected: visibility.value == 1,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Symbols.link_off),
|
||||||
|
title: Text('postVisibilityUnlisted'.tr()),
|
||||||
|
onTap: () {
|
||||||
|
visibility.value = 2;
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
selected: visibility.value == 2,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Symbols.lock),
|
||||||
|
title: Text('postVisibilityPrivate'.tr()),
|
||||||
|
onTap: () {
|
||||||
|
visibility.value = 3;
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
selected: visibility.value == 3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get the appropriate icon for each visibility status
|
||||||
|
IconData getVisibilityIcon(int visibilityValue) {
|
||||||
|
switch (visibilityValue) {
|
||||||
|
case 1: // Friends
|
||||||
|
return Symbols.group;
|
||||||
|
case 2: // Unlisted
|
||||||
|
return Symbols.link_off;
|
||||||
|
case 3: // Private
|
||||||
|
return Symbols.lock;
|
||||||
|
default: // Public (0) or unknown
|
||||||
|
return Symbols.public;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get the translation key for each visibility status
|
||||||
|
String getVisibilityText(int visibilityValue) {
|
||||||
|
switch (visibilityValue) {
|
||||||
|
case 1: // Friends
|
||||||
|
return 'postVisibilityFriends';
|
||||||
|
case 2: // Unlisted
|
||||||
|
return 'postVisibilityUnlisted';
|
||||||
|
case 3: // Private
|
||||||
|
return 'postVisibilityPrivate';
|
||||||
|
default: // Public (0) or unknown
|
||||||
|
return 'postVisibilityPublic';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,6 +392,48 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
if (repliedPost != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.reply, size: 16),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${'reply'.tr()}: ${repliedPost!.publisher.nick}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (forwardedPost != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.forward, size: 16),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${'forward'.tr()}: ${forwardedPost!.publisher.nick}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
@ -324,7 +462,52 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
showVisibilityModal();
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
vertical: -2,
|
||||||
|
horizontal: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
getVisibilityIcon(visibility.value),
|
||||||
|
size: 16,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
getVisibilityText(visibility.value).tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(bottom: 6),
|
||||||
TextField(
|
TextField(
|
||||||
controller: titleController,
|
controller: titleController,
|
||||||
decoration: InputDecoration.collapsed(
|
decoration: InputDecoration.collapsed(
|
||||||
@ -348,7 +531,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
RawKeyboardListener(
|
RawKeyboardListener(
|
||||||
focusNode: FocusNode(),
|
focusNode: FocusNode(),
|
||||||
onKey: _handleKeyPress,
|
onKey: handleKeyPress,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: contentController,
|
controller: contentController,
|
||||||
style: TextStyle(fontSize: 14),
|
style: TextStyle(fontSize: 14),
|
||||||
@ -474,204 +657,3 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AttachmentPreview extends StatelessWidget {
|
|
||||||
final UniversalFile item;
|
|
||||||
final double? progress;
|
|
||||||
final Function(int)? onMove;
|
|
||||||
final Function? onDelete;
|
|
||||||
final Function? onRequestUpload;
|
|
||||||
const AttachmentPreview({
|
|
||||||
super.key,
|
|
||||||
required this.item,
|
|
||||||
this.progress,
|
|
||||||
this.onRequestUpload,
|
|
||||||
this.onMove,
|
|
||||||
this.onDelete,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AspectRatio(
|
|
||||||
aspectRatio:
|
|
||||||
(item.isOnCloud ? (item.data.fileMeta?['ratio'] ?? 1) : 1).toDouble(),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (item.isOnCloud) {
|
|
||||||
return CloudFileWidget(item: item.data);
|
|
||||||
} else if (item.data is XFile) {
|
|
||||||
if (item.type == UniversalFileType.image) {
|
|
||||||
return Image.file(File(item.data.path));
|
|
||||||
} else {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
'Preview is not supported for ${item.type}',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (item is List<int> || item is Uint8List) {
|
|
||||||
if (item.type == UniversalFileType.image) {
|
|
||||||
return Image.memory(item.data);
|
|
||||||
} else {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
'Preview is not supported for ${item.type}',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Placeholder();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (progress != null)
|
|
||||||
Positioned.fill(
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withOpacity(0.3),
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (progress != null)
|
|
||||||
Text(
|
|
||||||
'${progress!.toStringAsFixed(2)}%',
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Text(
|
|
||||||
'uploading'.tr(),
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
Gap(6),
|
|
||||||
Center(child: LinearProgressIndicator(value: progress)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 8,
|
|
||||||
top: 8,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withOpacity(0.5),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (onDelete != null)
|
|
||||||
InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: const Icon(
|
|
||||||
Symbols.delete,
|
|
||||||
size: 14,
|
|
||||||
color: Colors.white,
|
|
||||||
).padding(horizontal: 8, vertical: 6),
|
|
||||||
onTap: () {
|
|
||||||
onDelete?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (onDelete != null && onMove != null)
|
|
||||||
SizedBox(
|
|
||||||
height: 26,
|
|
||||||
child: const VerticalDivider(
|
|
||||||
width: 0.3,
|
|
||||||
color: Colors.white,
|
|
||||||
thickness: 0.3,
|
|
||||||
),
|
|
||||||
).padding(horizontal: 2),
|
|
||||||
if (onMove != null)
|
|
||||||
InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: const Icon(
|
|
||||||
Symbols.keyboard_arrow_up,
|
|
||||||
size: 14,
|
|
||||||
color: Colors.white,
|
|
||||||
).padding(horizontal: 8, vertical: 6),
|
|
||||||
onTap: () {
|
|
||||||
onMove?.call(-1);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (onMove != null)
|
|
||||||
InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: const Icon(
|
|
||||||
Symbols.keyboard_arrow_down,
|
|
||||||
size: 14,
|
|
||||||
color: Colors.white,
|
|
||||||
).padding(horizontal: 8, vertical: 6),
|
|
||||||
onTap: () {
|
|
||||||
onMove?.call(1);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (onRequestUpload != null)
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
onTap: () => onRequestUpload?.call(),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withOpacity(0.5),
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
child:
|
|
||||||
(item.isOnCloud)
|
|
||||||
? Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.cloud,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
'On-cloud',
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.cloud_off,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
'On-device',
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:auto_route/annotations.dart';
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -215,7 +215,10 @@ class EditRealmScreen extends HookConsumerWidget {
|
|||||||
if (token == null) throw ArgumentError('Access token is null');
|
if (token == null) throw ArgumentError('Access token is null');
|
||||||
final cloudFile =
|
final cloudFile =
|
||||||
await putMediaToCloud(
|
await putMediaToCloud(
|
||||||
fileData: result,
|
fileData: UniversalFile(
|
||||||
|
data: result,
|
||||||
|
type: UniversalFileType.image,
|
||||||
|
),
|
||||||
atk: token,
|
atk: token,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
filename: result.name,
|
filename: result.name,
|
||||||
|
@ -39,7 +39,7 @@ Future<XFile?> cropImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Completer<SnCloudFile?> putMediaToCloud({
|
Completer<SnCloudFile?> putMediaToCloud({
|
||||||
required dynamic fileData, // Can be XFile or List<int> (Uint8List)
|
required UniversalFile fileData,
|
||||||
required String atk,
|
required String atk,
|
||||||
required String baseUrl,
|
required String baseUrl,
|
||||||
String? filename,
|
String? filename,
|
||||||
@ -51,21 +51,27 @@ Completer<SnCloudFile?> putMediaToCloud({
|
|||||||
String actualMimetype = mimetype ?? '';
|
String actualMimetype = mimetype ?? '';
|
||||||
Uint8List? byteData;
|
Uint8List? byteData;
|
||||||
|
|
||||||
if (fileData is XFile) {
|
// Handle the data based on what's in the UniversalFile
|
||||||
file = fileData;
|
final data = fileData.data;
|
||||||
actualFilename = filename ?? fileData.name;
|
|
||||||
actualMimetype = mimetype ?? fileData.mimeType ?? '';
|
if (data is XFile) {
|
||||||
} else if (fileData is List<int> || fileData is Uint8List) {
|
file = data;
|
||||||
byteData = fileData is List<int> ? Uint8List.fromList(fileData) : fileData;
|
actualFilename = filename ?? data.name;
|
||||||
|
actualMimetype = mimetype ?? data.mimeType ?? '';
|
||||||
|
} else if (data is List<int> || data is Uint8List) {
|
||||||
|
byteData = data is List<int> ? Uint8List.fromList(data) : data;
|
||||||
actualFilename = filename ?? 'uploaded_file';
|
actualFilename = filename ?? 'uploaded_file';
|
||||||
actualMimetype = mimetype ?? 'application/octet-stream';
|
actualMimetype = mimetype ?? 'application/octet-stream';
|
||||||
if (mimetype == null) {
|
if (mimetype == null) {
|
||||||
throw ArgumentError('Mimetype is required when providing raw bytes.');
|
throw ArgumentError('Mimetype is required when providing raw bytes.');
|
||||||
}
|
}
|
||||||
file = XFile.fromData(byteData!, mimeType: actualMimetype);
|
file = XFile.fromData(byteData!, mimeType: actualMimetype);
|
||||||
|
} else if (data is SnCloudFile) {
|
||||||
|
// If the file is already on the cloud, just return it
|
||||||
|
return Completer<SnCloudFile?>()..complete(data);
|
||||||
} else {
|
} else {
|
||||||
throw ArgumentError(
|
throw ArgumentError(
|
||||||
'Invalid fileData type. Expected XFile or List<int> (Uint8List).',
|
'Invalid fileData type. Expected data to be XFile, List<int>, Uint8List, or SnCloudFile.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,7 +414,7 @@ class _MessageItemContent extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
case 'text':
|
case 'text':
|
||||||
default:
|
default:
|
||||||
return MarkdownTextContent(content: item.content!);
|
return MarkdownTextContent(content: item.content!, isSelectable: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
212
lib/widgets/content/attachment_preview.dart
Normal file
212
lib/widgets/content/attachment_preview.dart
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class AttachmentPreview extends StatelessWidget {
|
||||||
|
final UniversalFile item;
|
||||||
|
final double? progress;
|
||||||
|
final Function(int)? onMove;
|
||||||
|
final Function? onDelete;
|
||||||
|
final Function? onRequestUpload;
|
||||||
|
const AttachmentPreview({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
this.progress,
|
||||||
|
this.onRequestUpload,
|
||||||
|
this.onMove,
|
||||||
|
this.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AspectRatio(
|
||||||
|
aspectRatio:
|
||||||
|
(item.isOnCloud ? (item.data.fileMeta?['ratio'] ?? 1) : 1).toDouble(),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (item.isOnCloud) {
|
||||||
|
return CloudFileWidget(item: item.data);
|
||||||
|
} else if (item.data is XFile) {
|
||||||
|
if (item.type == UniversalFileType.image) {
|
||||||
|
return Image.file(File(item.data.path));
|
||||||
|
} else {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'Preview is not supported for ${item.type}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (item is List<int> || item is Uint8List) {
|
||||||
|
if (item.type == UniversalFileType.image) {
|
||||||
|
return Image.memory(item.data);
|
||||||
|
} else {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'Preview is not supported for ${item.type}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Placeholder();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (progress != null)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (progress != null)
|
||||||
|
Text(
|
||||||
|
'${progress!.toStringAsFixed(2)}%',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
'uploading'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Gap(6),
|
||||||
|
Center(child: LinearProgressIndicator(value: progress)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 8,
|
||||||
|
top: 8,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (onDelete != null)
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: const Icon(
|
||||||
|
Symbols.delete,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
).padding(horizontal: 8, vertical: 6),
|
||||||
|
onTap: () {
|
||||||
|
onDelete?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (onDelete != null && onMove != null)
|
||||||
|
SizedBox(
|
||||||
|
height: 26,
|
||||||
|
child: const VerticalDivider(
|
||||||
|
width: 0.3,
|
||||||
|
color: Colors.white,
|
||||||
|
thickness: 0.3,
|
||||||
|
),
|
||||||
|
).padding(horizontal: 2),
|
||||||
|
if (onMove != null)
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: const Icon(
|
||||||
|
Symbols.keyboard_arrow_up,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
).padding(horizontal: 8, vertical: 6),
|
||||||
|
onTap: () {
|
||||||
|
onMove?.call(-1);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (onMove != null)
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: const Icon(
|
||||||
|
Symbols.keyboard_arrow_down,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
).padding(horizontal: 8, vertical: 6),
|
||||||
|
onTap: () {
|
||||||
|
onMove?.call(1);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onRequestUpload != null)
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onTap: () => onRequestUpload?.call(),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child:
|
||||||
|
(item.isOnCloud)
|
||||||
|
? Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.cloud,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'On-cloud',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.cloud_off,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'On-device',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,9 +8,9 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
|
||||||
import 'package:island/services/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/attachment_preview.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ class PostItem extends HookConsumerWidget {
|
|||||||
final SnPost item;
|
final SnPost item;
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
final bool isOpenable;
|
final bool isOpenable;
|
||||||
|
final bool showReferencePost;
|
||||||
final Function? onRefresh;
|
final Function? onRefresh;
|
||||||
final Function(SnPost)? onUpdate;
|
final Function(SnPost)? onUpdate;
|
||||||
const PostItem({
|
const PostItem({
|
||||||
@ -33,6 +34,7 @@ class PostItem extends HookConsumerWidget {
|
|||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.isOpenable = true,
|
this.isOpenable = true,
|
||||||
|
this.showReferencePost = true,
|
||||||
this.onRefresh,
|
this.onRefresh,
|
||||||
this.onUpdate,
|
this.onUpdate,
|
||||||
});
|
});
|
||||||
@ -101,6 +103,20 @@ class PostItem extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
MenuAction(
|
||||||
|
title: 'reply'.tr(),
|
||||||
|
image: MenuImage.icon(Symbols.reply),
|
||||||
|
callback: () {
|
||||||
|
context.router.push(PostComposeRoute(repliedPost: item));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuAction(
|
||||||
|
title: 'forward'.tr(),
|
||||||
|
image: MenuImage.icon(Symbols.forward),
|
||||||
|
callback: () {
|
||||||
|
context.router.push(PostComposeRoute(forwardedPost: item));
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -132,8 +148,52 @@ class PostItem extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(item.publisher.nick).bold(),
|
Text(item.publisher.nick).bold(),
|
||||||
|
// Add visibility indicator if not public (visibility != 0)
|
||||||
|
if (item.visibility != 0)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_getVisibilityIcon(item.visibility),
|
||||||
|
size: 14,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_getVisibilityText(item.visibility).tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(top: 2, bottom: 2),
|
||||||
|
if (item.title?.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
item.title!,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
if (item.description?.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
item.description!,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
).padding(bottom: 8),
|
||||||
if (item.content?.isNotEmpty ?? false)
|
if (item.content?.isNotEmpty ?? false)
|
||||||
MarkdownTextContent(content: item.content!),
|
MarkdownTextContent(content: item.content!),
|
||||||
|
if ((item.repliedPost != null ||
|
||||||
|
item.forwardedPost != null) &&
|
||||||
|
showReferencePost)
|
||||||
|
_buildReferencePost(context, item),
|
||||||
if (item.attachments.isNotEmpty)
|
if (item.attachments.isNotEmpty)
|
||||||
CloudFileList(
|
CloudFileList(
|
||||||
files: item.attachments,
|
files: item.attachments,
|
||||||
@ -178,6 +238,141 @@ class PostItem extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildReferencePost(BuildContext context, SnPost item) {
|
||||||
|
final referencePost = item.repliedPost ?? item.forwardedPost;
|
||||||
|
if (referencePost == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final isReply = item.repliedPost != null;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(top: 8, bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isReply ? Symbols.reply : Symbols.forward,
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
isReply ? 'repliedTo'.tr() : 'forwarded'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ProfilePictureWidget(
|
||||||
|
fileId: referencePost.publisher.picture?.id,
|
||||||
|
radius: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
referencePost.publisher.nick,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Add visibility indicator for referenced post if not public
|
||||||
|
if (referencePost.visibility != 0)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_getVisibilityIcon(referencePost.visibility),
|
||||||
|
size: 12,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_getVisibilityText(referencePost.visibility).tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(top: 2, bottom: 2),
|
||||||
|
if (referencePost.title?.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
referencePost.title!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
).padding(top: 2, bottom: 2),
|
||||||
|
if (referencePost.description?.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
referencePost.description!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).padding(bottom: 2),
|
||||||
|
if (referencePost.content?.isNotEmpty ?? false)
|
||||||
|
MarkdownTextContent(
|
||||||
|
content: referencePost.content!,
|
||||||
|
textStyle: const TextStyle(fontSize: 14),
|
||||||
|
isSelectable: false,
|
||||||
|
).padding(bottom: 4),
|
||||||
|
if (referencePost.attachments.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.attach_file,
|
||||||
|
size: 12,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'postHasAttachments'.plural(
|
||||||
|
referencePost.attachments.length,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(vertical: 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).gestures(
|
||||||
|
onTap: () => context.router.push(PostDetailRoute(id: referencePost.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class PostReactionList extends HookConsumerWidget {
|
class PostReactionList extends HookConsumerWidget {
|
||||||
final String parentId;
|
final String parentId;
|
||||||
final Map<String, int> reactions;
|
final Map<String, int> reactions;
|
||||||
@ -388,3 +583,31 @@ class _PostReactionSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to get the appropriate icon for each visibility status
|
||||||
|
IconData _getVisibilityIcon(int visibility) {
|
||||||
|
switch (visibility) {
|
||||||
|
case 1: // Friends
|
||||||
|
return Symbols.group;
|
||||||
|
case 2: // Unlisted
|
||||||
|
return Symbols.link_off;
|
||||||
|
case 3: // Private
|
||||||
|
return Symbols.lock;
|
||||||
|
default: // Public (0) or unknown
|
||||||
|
return Symbols.public;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get the translation key for each visibility status
|
||||||
|
String _getVisibilityText(int visibility) {
|
||||||
|
switch (visibility) {
|
||||||
|
case 1: // Friends
|
||||||
|
return 'postVisibilityFriends';
|
||||||
|
case 2: // Unlisted
|
||||||
|
return 'postVisibilityUnlisted';
|
||||||
|
case 3: // Private
|
||||||
|
return 'postVisibilityPrivate';
|
||||||
|
default: // Public (0) or unknown
|
||||||
|
return 'postVisibilityPublic';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,8 +14,6 @@ class PostListNotifier extends _$PostListNotifier
|
|||||||
with CursorPagingNotifierMixin<SnPost> {
|
with CursorPagingNotifierMixin<SnPost> {
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
String? pubName;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<SnPost>> build(String? pubName) {
|
Future<CursorPagingData<SnPost>> build(String? pubName) {
|
||||||
this.pubName = pubName;
|
this.pubName = pubName;
|
||||||
|
@ -6,7 +6,7 @@ part of 'post_list.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$postListNotifierHash() => r'6568b7a5afad71551009d9bc7af26afb4b07c9e5';
|
String _$postListNotifierHash() => r'58a2d5d9a8f742f0a3a3e224a51a811d43903e0d';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
@ -94,6 +94,7 @@ class PostRepliesList extends HookConsumerWidget {
|
|||||||
PostItem(
|
PostItem(
|
||||||
item: data.items[index],
|
item: data.items[index],
|
||||||
backgroundColor: isWide ? Colors.transparent : null,
|
backgroundColor: isWide ? Colors.transparent : null,
|
||||||
|
showReferencePost: false,
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin.h>
|
#include <media_kit_video/media_kit_video_plugin.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
|
#include <record_linux/record_linux_plugin.h>
|
||||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
#include <super_native_extensions/super_native_extensions_plugin.h>
|
#include <super_native_extensions/super_native_extensions_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
@ -48,6 +49,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
|
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
|
||||||
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
|
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) record_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
|
||||||
|
record_linux_plugin_register_with_registrar(record_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||||
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
||||||
|
@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
media_kit_video
|
media_kit_video
|
||||||
pasteboard
|
pasteboard
|
||||||
|
record_linux
|
||||||
sqlite3_flutter_libs
|
sqlite3_flutter_libs
|
||||||
super_native_extensions
|
super_native_extensions
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
@ -23,6 +23,7 @@ import media_kit_video
|
|||||||
import package_info_plus
|
import package_info_plus
|
||||||
import pasteboard
|
import pasteboard
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import record_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import sqlite3_flutter_libs
|
import sqlite3_flutter_libs
|
||||||
@ -50,6 +51,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||||
|
64
pubspec.lock
64
pubspec.lock
@ -1550,6 +1550,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
|
record:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: record
|
||||||
|
sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
record_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_android
|
||||||
|
sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
record_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_ios
|
||||||
|
sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
record_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_linux
|
||||||
|
sha256: "29e7735b05c1944bb6c9b72a36c08d4a1b24117e712d6a9523c003bde12bf484"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
record_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_macos
|
||||||
|
sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
record_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_platform_interface
|
||||||
|
sha256: "8a575828733d4c3cb5983c914696f40db8667eab3538d4c41c50cbb79e722ef4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
record_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_web
|
||||||
|
sha256: f8e536a9c927e52f95326d7540898457eaeefbe0b21a84d3cb3d2d7d4645e8cb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.7"
|
||||||
|
record_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_windows
|
||||||
|
sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
relative_time:
|
relative_time:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
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: 3.0.0+100
|
version: 3.0.0+102
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
@ -104,6 +104,7 @@ dependencies:
|
|||||||
livekit_client: ^2.4.7
|
livekit_client: ^2.4.7
|
||||||
pasteboard: ^0.4.0
|
pasteboard: ^0.4.0
|
||||||
flutter_colorpicker: ^1.1.0
|
flutter_colorpicker: ^1.1.0
|
||||||
|
record: ^6.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
|
#include <record_windows/record_windows_plugin_c_api.h>
|
||||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
|
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
@ -51,6 +52,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||||
PasteboardPluginRegisterWithRegistrar(
|
PasteboardPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||||
|
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||||
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
|
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
|
||||||
|
@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
media_kit_libs_windows_video
|
media_kit_libs_windows_video
|
||||||
media_kit_video
|
media_kit_video
|
||||||
pasteboard
|
pasteboard
|
||||||
|
record_windows
|
||||||
sqlite3_flutter_libs
|
sqlite3_flutter_libs
|
||||||
super_native_extensions
|
super_native_extensions
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
Reference in New Issue
Block a user