Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
b84fafb53c | |||
9e9d8b6fab | |||
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",
|
||||
"reply": "Reply",
|
||||
"forward": "Forward",
|
||||
"repliedTo": "Replied to",
|
||||
"forwarded": "Forwarded",
|
||||
"hasAttachments": {
|
||||
"one": "{} attachment",
|
||||
"other": "{} attachments"
|
||||
},
|
||||
"postHasAttachments": {
|
||||
"one": "{} attachment",
|
||||
"other": "{} attachments"
|
||||
},
|
||||
"edited": "Edited",
|
||||
"addVideo": "Add video",
|
||||
"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.",
|
||||
"unauthorized": "Unauthorized",
|
||||
"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": "成员",
|
||||
"reply": "回复",
|
||||
"forward": "转发",
|
||||
"repliedTo": "回复了",
|
||||
"forwarded": "转发了",
|
||||
"hasAttachments": {
|
||||
"other": "{}个附件"
|
||||
},
|
||||
"postHasAttachments": {
|
||||
"other": "{}个附件"
|
||||
},
|
||||
"edited": "已编辑",
|
||||
"addVideo": "添加视频",
|
||||
"addPhoto": "添加照片",
|
||||
@ -278,5 +286,10 @@
|
||||
"settingsHideBottomNav": "隐藏底部导航",
|
||||
"settingsSoundEffects": "音效",
|
||||
"settingsAprilFoolFeatures": "愚人节功能",
|
||||
"settingsEnterToSend": "按下 Enter 发送"
|
||||
"settingsEnterToSend": "按下 Enter 发送",
|
||||
"postVisibility": "可见性",
|
||||
"postVisibilityPublic": "公开",
|
||||
"postVisibilityFriends": "仅好友可见",
|
||||
"postVisibilityUnlisted": "不公开",
|
||||
"postVisibilityPrivate": "私密"
|
||||
}
|
@ -99,6 +99,14 @@
|
||||
"permissionMember": "成員",
|
||||
"reply": "回覆",
|
||||
"forward": "轉發",
|
||||
"repliedTo": "回覆了",
|
||||
"forwarded": "轉發了",
|
||||
"hasAttachments": {
|
||||
"other": "{}個附件"
|
||||
},
|
||||
"postHasAttachments": {
|
||||
"other": "{}個附件"
|
||||
},
|
||||
"edited": "已編輯",
|
||||
"addVideo": "新增影片",
|
||||
"addPhoto": "新增照片",
|
||||
@ -278,5 +286,10 @@
|
||||
"settingsHideBottomNav": "隱藏底部導航",
|
||||
"settingsSoundEffects": "音效",
|
||||
"settingsAprilFoolFeatures": "愚人節功能",
|
||||
"settingsEnterToSend": "按下 Enter 傳送"
|
||||
"settingsEnterToSend": "按下 Enter 傳送",
|
||||
"postVisibility": "可見性",
|
||||
"postVisibilityPublic": "公開",
|
||||
"postVisibilityFriends": "僅好友可見",
|
||||
"postVisibilityUnlisted": "不公開",
|
||||
"postVisibilityPrivate": "私密"
|
||||
}
|
@ -90,6 +90,8 @@ PODS:
|
||||
- flutter_webrtc (0.14.0):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.07)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
@ -144,6 +146,8 @@ PODS:
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- record_ios (1.0.0):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
@ -201,6 +205,7 @@ DEPENDENCIES:
|
||||
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
- Kingfisher (~> 8.0)
|
||||
@ -210,6 +215,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
@ -265,6 +271,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||
flutter_webrtc:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
gal:
|
||||
:path: ".symlinks/plugins/gal/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
irondash_engine_context:
|
||||
@ -281,6 +289,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/pasteboard/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
record_ios:
|
||||
:path: ".symlinks/plugins/record_ios/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
@ -317,6 +327,7 @@ SPEC CHECKSUMS:
|
||||
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117
|
||||
gal: 29e711cd17bccb47f839d9b30afe9bc9750950b2
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
@ -331,6 +342,7 @@ SPEC CHECKSUMS:
|
||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
|
@ -113,6 +113,7 @@
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
CloudFile.swift,
|
||||
DataExchange.swift,
|
||||
);
|
||||
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import UIKit
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
@ -66,5 +66,10 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INStartCallIntent</string>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -16,17 +16,12 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
var token: String = ""
|
||||
if let tokenJson = UserDefaults.standard.string(forKey: "dyn_user_tk"),
|
||||
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 {
|
||||
var token: String? = UserDefaults.standard.getFlutterToken()
|
||||
if token == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let serverUrl = "https://nt.solian.app"
|
||||
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
|
||||
|
||||
let parameters: [String: Any?] = [
|
||||
@ -35,7 +30,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
|
||||
]
|
||||
|
||||
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()
|
||||
.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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INStartCallIntent</string>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
@ -231,7 +231,7 @@ class MessageRepository {
|
||||
for (var idx = 0; idx < attachments.length; idx++) {
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: attachments[idx].data,
|
||||
fileData: attachments[idx],
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: attachments[idx].data.name ?? 'Post media',
|
||||
|
@ -1071,10 +1071,17 @@ class PostComposeRoute extends _i27.PageRouteInfo<PostComposeRouteArgs> {
|
||||
PostComposeRoute({
|
||||
_i28.Key? key,
|
||||
_i30.SnPost? originalPost,
|
||||
_i30.SnPost? repliedPost,
|
||||
_i30.SnPost? forwardedPost,
|
||||
List<_i27.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
PostComposeRoute.name,
|
||||
args: PostComposeRouteArgs(key: key, originalPost: originalPost),
|
||||
args: PostComposeRouteArgs(
|
||||
key: key,
|
||||
originalPost: originalPost,
|
||||
repliedPost: repliedPost,
|
||||
forwardedPost: forwardedPost,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
@ -1089,32 +1096,50 @@ class PostComposeRoute extends _i27.PageRouteInfo<PostComposeRouteArgs> {
|
||||
return _i18.PostComposeScreen(
|
||||
key: args.key,
|
||||
originalPost: args.originalPost,
|
||||
repliedPost: args.repliedPost,
|
||||
forwardedPost: args.forwardedPost,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class PostComposeRouteArgs {
|
||||
const PostComposeRouteArgs({this.key, this.originalPost});
|
||||
const PostComposeRouteArgs({
|
||||
this.key,
|
||||
this.originalPost,
|
||||
this.repliedPost,
|
||||
this.forwardedPost,
|
||||
});
|
||||
|
||||
final _i28.Key? key;
|
||||
|
||||
final _i30.SnPost? originalPost;
|
||||
|
||||
final _i30.SnPost? repliedPost;
|
||||
|
||||
final _i30.SnPost? forwardedPost;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost}';
|
||||
return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost, repliedPost: $repliedPost, forwardedPost: $forwardedPost}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
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
|
||||
int get hashCode => key.hashCode ^ originalPost.hashCode;
|
||||
int get hashCode =>
|
||||
key.hashCode ^
|
||||
originalPost.hashCode ^
|
||||
repliedPost.hashCode ^
|
||||
forwardedPost.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
@ -63,7 +64,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: result,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
|
@ -527,7 +527,10 @@ class EditChatScreen extends HookConsumerWidget {
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: result,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
|
@ -17,12 +17,12 @@ import 'package:island/pods/database.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/call_overlay.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/response.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
@ -514,7 +514,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const Text('Loading...'),
|
||||
error:
|
||||
(err, __) => ResponseErrorWidget(
|
||||
(err, _) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => messagesNotifier.loadInitial(),
|
||||
),
|
||||
@ -615,7 +615,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
progress: null,
|
||||
showAvatar: false,
|
||||
),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -680,7 +680,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
attachments.value = newAttachments;
|
||||
},
|
||||
),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
@ -788,7 +788,7 @@ class _ChatInput extends ConsumerWidget {
|
||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
).padding(top: 12),
|
||||
if (messageReplyingTo != null ||
|
||||
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
@ -99,7 +100,10 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: result,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
|
@ -298,7 +298,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
client.delete('/stickers/$packId');
|
||||
ref.invalidate(stickerPacksProvider);
|
||||
ref.invalidate(stickerPacksNotifierProvider);
|
||||
if (context.mounted) context.router.maybePop(true);
|
||||
}
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
|
||||
part 'stickers.g.dart';
|
||||
|
||||
@ -24,9 +24,6 @@ class StickersScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final stickersState = ref.watch(stickerPacksProvider);
|
||||
final stickersNotifier = ref.watch(stickerPacksProvider.notifier);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('stickers').tr(),
|
||||
@ -37,7 +34,7 @@ class StickersScreen extends HookConsumerWidget {
|
||||
value,
|
||||
) {
|
||||
if (value != null) {
|
||||
stickersNotifier.refresh();
|
||||
ref.invalidate(stickerPacksNotifierProvider(pubName));
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -46,104 +43,90 @@ class StickersScreen extends HookConsumerWidget {
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: stickersState.when(
|
||||
data:
|
||||
(stickers) => RefreshIndicator(
|
||||
onRefresh: stickersNotifier.refresh,
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: stickers.length,
|
||||
hasReachedMax: stickersNotifier.isReachedMax,
|
||||
isLoading: stickersNotifier.isLoading,
|
||||
onFetchData: stickersNotifier.fetchMore,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(stickers[index].name),
|
||||
subtitle: Text(stickers[index].description),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context.router.push(
|
||||
StickerPackDetailRoute(
|
||||
pubName: pubName,
|
||||
id: stickers[index].id,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (error, stack) => Text('Error: $error'),
|
||||
),
|
||||
body: SliverStickerPacksList(pubName: pubName),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final stickerPacksProvider = StateNotifierProvider<
|
||||
StickerPacksNotifier,
|
||||
AsyncValue<List<SnStickerPack>>
|
||||
>((ref) {
|
||||
return StickerPacksNotifier(ref.watch(apiClientProvider));
|
||||
});
|
||||
class SliverStickerPacksList extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
const SliverStickerPacksList({super.key, required this.pubName});
|
||||
|
||||
class StickerPacksNotifier
|
||||
extends StateNotifier<AsyncValue<List<SnStickerPack>>> {
|
||||
final Dio _apiClient;
|
||||
StickerPacksNotifier(this._apiClient) : super(const AsyncValue.loading()) {
|
||||
fetchStickers();
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return PagingHelperView(
|
||||
provider: stickerPacksNotifierProvider(pubName),
|
||||
futureRefreshable: stickerPacksNotifierProvider(pubName).future,
|
||||
notifierRefreshable: stickerPacksNotifierProvider(pubName).notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final sticker = data.items[index];
|
||||
return ListTile(
|
||||
title: Text(sticker.name),
|
||||
subtitle: Text(sticker.description),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context.router.push(
|
||||
StickerPackDetailRoute(pubName: pubName, id: sticker.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class StickerPacksNotifier extends _$StickerPacksNotifier
|
||||
with CursorPagingNotifierMixin<SnStickerPack> {
|
||||
static const int _pageSize = 20;
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnStickerPack>> build(String pubName) {
|
||||
return fetch(cursor: null);
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
int take = 20;
|
||||
int total = 0;
|
||||
|
||||
bool isLoading = false;
|
||||
bool get isReachedMax =>
|
||||
state.valueOrNull != null && state.valueOrNull!.length >= total;
|
||||
|
||||
Future<void> fetchStickers() async {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
@override
|
||||
Future<CursorPagingData<SnStickerPack>> fetch({
|
||||
required String? cursor,
|
||||
}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/stickers?offset=$offset&take=$take',
|
||||
final response = await client.get(
|
||||
'/stickers',
|
||||
queryParameters: {
|
||||
'offset': offset,
|
||||
'take': _pageSize,
|
||||
'pubName': pubName,
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final newStickers =
|
||||
response.data
|
||||
.map((e) => SnStickerPack.fromJson(e))
|
||||
.cast<SnStickerPack>()
|
||||
.toList();
|
||||
|
||||
state = AsyncValue.data(
|
||||
state.valueOrNull != null
|
||||
? [...state.value!, ...newStickers]
|
||||
: newStickers,
|
||||
);
|
||||
offset += take;
|
||||
} else {
|
||||
state = AsyncValue.error('Failed to load stickers', StackTrace.current);
|
||||
}
|
||||
} catch (err, stackTrace) {
|
||||
state = AsyncValue.error(err, stackTrace);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final List<dynamic> data = response.data;
|
||||
final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList();
|
||||
|
||||
final hasMore = offset + stickers.length < total;
|
||||
final nextCursor = hasMore ? (offset + stickers.length).toString() : null;
|
||||
|
||||
return CursorPagingData(
|
||||
items: stickers,
|
||||
hasMore: hasMore,
|
||||
nextCursor: nextCursor,
|
||||
);
|
||||
} catch (err) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.valueOrNull == null || state.valueOrNull!.length >= total) return;
|
||||
await fetchStickers();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
offset = 0;
|
||||
state = const AsyncValue.loading();
|
||||
await fetchStickers();
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
|
@ -147,5 +147,154 @@ class _StickerPackProviderElement
|
||||
String? get packId => (origin as StickerPackProvider).packId;
|
||||
}
|
||||
|
||||
String _$stickerPacksNotifierHash() =>
|
||||
r'2feff50a7896eb8759fe91e9626b0409354d9fee';
|
||||
|
||||
abstract class _$StickerPacksNotifier
|
||||
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> {
|
||||
late final String pubName;
|
||||
|
||||
FutureOr<CursorPagingData<SnStickerPack>> build(String pubName);
|
||||
}
|
||||
|
||||
/// See also [StickerPacksNotifier].
|
||||
@ProviderFor(StickerPacksNotifier)
|
||||
const stickerPacksNotifierProvider = StickerPacksNotifierFamily();
|
||||
|
||||
/// See also [StickerPacksNotifier].
|
||||
class StickerPacksNotifierFamily
|
||||
extends Family<AsyncValue<CursorPagingData<SnStickerPack>>> {
|
||||
/// See also [StickerPacksNotifier].
|
||||
const StickerPacksNotifierFamily();
|
||||
|
||||
/// See also [StickerPacksNotifier].
|
||||
StickerPacksNotifierProvider call(String pubName) {
|
||||
return StickerPacksNotifierProvider(pubName);
|
||||
}
|
||||
|
||||
@override
|
||||
StickerPacksNotifierProvider getProviderOverride(
|
||||
covariant StickerPacksNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.pubName);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'stickerPacksNotifierProvider';
|
||||
}
|
||||
|
||||
/// See also [StickerPacksNotifier].
|
||||
class StickerPacksNotifierProvider
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderImpl<
|
||||
StickerPacksNotifier,
|
||||
CursorPagingData<SnStickerPack>
|
||||
> {
|
||||
/// See also [StickerPacksNotifier].
|
||||
StickerPacksNotifierProvider(String pubName)
|
||||
: this._internal(
|
||||
() => StickerPacksNotifier()..pubName = pubName,
|
||||
from: stickerPacksNotifierProvider,
|
||||
name: r'stickerPacksNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$stickerPacksNotifierHash,
|
||||
dependencies: StickerPacksNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
StickerPacksNotifierFamily._allTransitiveDependencies,
|
||||
pubName: pubName,
|
||||
);
|
||||
|
||||
StickerPacksNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.pubName,
|
||||
}) : super.internal();
|
||||
|
||||
final String pubName;
|
||||
|
||||
@override
|
||||
FutureOr<CursorPagingData<SnStickerPack>> runNotifierBuild(
|
||||
covariant StickerPacksNotifier notifier,
|
||||
) {
|
||||
return notifier.build(pubName);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(StickerPacksNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: StickerPacksNotifierProvider._internal(
|
||||
() => create()..pubName = pubName,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
pubName: pubName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<
|
||||
StickerPacksNotifier,
|
||||
CursorPagingData<SnStickerPack>
|
||||
>
|
||||
createElement() {
|
||||
return _StickerPacksNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is StickerPacksNotifierProvider && other.pubName == pubName;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin StickerPacksNotifierRef
|
||||
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnStickerPack>> {
|
||||
/// The parameter `pubName` of this provider.
|
||||
String get pubName;
|
||||
}
|
||||
|
||||
class _StickerPacksNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderElement<
|
||||
StickerPacksNotifier,
|
||||
CursorPagingData<SnStickerPack>
|
||||
>
|
||||
with StickerPacksNotifierRef {
|
||||
_StickerPacksNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get pubName => (origin as StickerPacksNotifierProvider).pubName;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.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/app_scaffold.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:material_symbols_icons/symbols.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
@ -53,7 +52,14 @@ class PostEditScreen extends HookConsumerWidget {
|
||||
@RoutePage()
|
||||
class PostComposeScreen extends HookConsumerWidget {
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -90,9 +96,14 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
text: originalPost?.description,
|
||||
);
|
||||
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);
|
||||
|
||||
Future<void> pickPhotoMedia() async {
|
||||
@ -131,7 +142,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
attachmentProgress.value = {...attachmentProgress.value, index: 0};
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: attachment.data,
|
||||
fileData: attachment,
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: attachment.data.name ?? 'Post media',
|
||||
@ -188,12 +199,18 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
await client.request(
|
||||
originalPost == null ? '/posts' : '/posts/${originalPost!.id}',
|
||||
data: {
|
||||
'title': titleController.text,
|
||||
'description': descriptionController.text,
|
||||
'content': contentController.text,
|
||||
'visibility':
|
||||
visibility.value, // Add visibility field to API request
|
||||
'attachments':
|
||||
attachments.value
|
||||
.where((e) => e.isOnCloud)
|
||||
.map((e) => e.data.id)
|
||||
.toList(),
|
||||
if (repliedPost != null) 'replied_post_id': repliedPost!.id,
|
||||
if (forwardedPost != null) 'forwarded_post_id': forwardedPost!.id,
|
||||
},
|
||||
options: Options(
|
||||
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;
|
||||
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;
|
||||
|
||||
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
||||
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
@ -324,7 +462,52 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
controller: titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
@ -348,7 +531,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
const Gap(8),
|
||||
RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: _handleKeyPress,
|
||||
onKey: handleKeyPress,
|
||||
child: TextField(
|
||||
controller: contentController,
|
||||
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:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -215,7 +215,10 @@ class EditRealmScreen extends HookConsumerWidget {
|
||||
if (token == null) throw ArgumentError('Access token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: result,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
|
@ -39,7 +39,7 @@ Future<XFile?> cropImage(
|
||||
}
|
||||
|
||||
Completer<SnCloudFile?> putMediaToCloud({
|
||||
required dynamic fileData, // Can be XFile or List<int> (Uint8List)
|
||||
required UniversalFile fileData,
|
||||
required String atk,
|
||||
required String baseUrl,
|
||||
String? filename,
|
||||
@ -51,21 +51,27 @@ Completer<SnCloudFile?> putMediaToCloud({
|
||||
String actualMimetype = mimetype ?? '';
|
||||
Uint8List? byteData;
|
||||
|
||||
if (fileData is XFile) {
|
||||
file = fileData;
|
||||
actualFilename = filename ?? fileData.name;
|
||||
actualMimetype = mimetype ?? fileData.mimeType ?? '';
|
||||
} else if (fileData is List<int> || fileData is Uint8List) {
|
||||
byteData = fileData is List<int> ? Uint8List.fromList(fileData) : fileData;
|
||||
// Handle the data based on what's in the UniversalFile
|
||||
final data = fileData.data;
|
||||
|
||||
if (data is XFile) {
|
||||
file = data;
|
||||
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';
|
||||
actualMimetype = mimetype ?? 'application/octet-stream';
|
||||
if (mimetype == null) {
|
||||
throw ArgumentError('Mimetype is required when providing raw bytes.');
|
||||
}
|
||||
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 {
|
||||
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':
|
||||
default:
|
||||
return MarkdownTextContent(content: item.content!);
|
||||
return MarkdownTextContent(content: item.content!, isSelectable: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
228
lib/widgets/content/attachment_preview.dart
Normal file
228
lib/widgets/content/attachment_preview.dart
Normal file
@ -0,0 +1,228 @@
|
||||
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) {
|
||||
final file = item.data as XFile;
|
||||
if (file.path.isEmpty) {
|
||||
return FutureBuilder<Uint8List>(
|
||||
future: file.readAsBytes(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Image.memory(snapshot.data!);
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return kIsWeb
|
||||
? Image.network(file.path)
|
||||
: Image.file(File(file.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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,13 +6,17 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
class CloudFileList extends HookConsumerWidget {
|
||||
final List<SnCloudFile> files;
|
||||
@ -110,6 +114,7 @@ class CloudFileList extends HookConsumerWidget {
|
||||
heroTag: heroTags[i],
|
||||
isImage: files[i].mimeType?.startsWith('image') ?? false,
|
||||
disableZoomIn: disableZoomIn,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
],
|
||||
onTap: (i) {
|
||||
@ -175,6 +180,47 @@ class CloudFileZoomIn extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final photoViewController = useMemoized(() => PhotoViewController(), []);
|
||||
final rotation = useState(0);
|
||||
|
||||
Future<void> saveToGallery() async {
|
||||
try {
|
||||
// Show loading indicator
|
||||
final scaffold = ScaffoldMessenger.of(context);
|
||||
scaffold.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Saving image to gallery...'),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
|
||||
// Get the image URL
|
||||
final imageUrl = '$serverUrl/files/${item.id}?original=true';
|
||||
|
||||
// Create a temporary file to save the image
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}';
|
||||
|
||||
await Dio().download(imageUrl, filePath);
|
||||
await Gal.putImage(filePath);
|
||||
|
||||
// Show success message
|
||||
scaffold.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Image saved to gallery'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// Show error message
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to save image: $e'),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return DismissiblePage(
|
||||
isFullScreen: true,
|
||||
@ -195,16 +241,119 @@ class CloudFileZoomIn extends HookConsumerWidget {
|
||||
imageProvider: CloudImageWidget.provider(
|
||||
fileId: item.id,
|
||||
serverUrl: serverUrl,
|
||||
original: true,
|
||||
),
|
||||
// Apply rotation transformation
|
||||
customSize: MediaQuery.of(context).size,
|
||||
basePosition: Alignment.center,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
),
|
||||
// Close button
|
||||
// Close button and save button
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 20,
|
||||
right: 20,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
right: 16,
|
||||
left: 16,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.save_alt,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black54,
|
||||
blurRadius: 5.0,
|
||||
offset: Offset(1.0, 1.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
saveToGallery();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black54,
|
||||
blurRadius: 5.0,
|
||||
offset: Offset(1.0, 1.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Rotation controls
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.remove, color: Colors.white),
|
||||
onPressed: () {
|
||||
photoViewController.scale =
|
||||
(photoViewController.scale ?? 1) - 0.05;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.add, color: Colors.white),
|
||||
onPressed: () {
|
||||
photoViewController.scale =
|
||||
(photoViewController.scale ?? 1) + 0.05;
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.rotate_left,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black54,
|
||||
blurRadius: 5.0,
|
||||
offset: Offset(1.0, 1.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
rotation.value = (rotation.value - 1) % 4;
|
||||
photoViewController.rotation =
|
||||
rotation.value * -math.pi / 2;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.rotate_right,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black54,
|
||||
blurRadius: 5.0,
|
||||
offset: Offset(1.0, 1.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
rotation.value = (rotation.value + 1) % 4;
|
||||
photoViewController.rotation =
|
||||
rotation.value * -math.pi / 2;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -219,6 +368,7 @@ class _CloudFileListEntry extends StatelessWidget {
|
||||
final bool isImage;
|
||||
final bool disableZoomIn;
|
||||
final VoidCallback? onTap;
|
||||
final BoxFit fit;
|
||||
|
||||
const _CloudFileListEntry({
|
||||
required this.file,
|
||||
@ -226,11 +376,13 @@ class _CloudFileListEntry extends StatelessWidget {
|
||||
required this.isImage,
|
||||
required this.disableZoomIn,
|
||||
this.onTap,
|
||||
this.fit = BoxFit.contain,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (isImage)
|
||||
Positioned.fill(
|
||||
@ -247,9 +399,10 @@ class _CloudFileListEntry extends StatelessWidget {
|
||||
item: file,
|
||||
heroTag: heroTag,
|
||||
noBlurhash: true,
|
||||
).center()
|
||||
fit: fit,
|
||||
)
|
||||
else
|
||||
CloudFileWidget(item: file, heroTag: heroTag),
|
||||
CloudFileWidget(item: file, heroTag: heroTag, fit: fit),
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -8,9 +8,9 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@ -56,7 +56,7 @@ class CloudFilePicker extends HookConsumerWidget {
|
||||
final file = files.value[idx];
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: file.data,
|
||||
fileData: file,
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: file.data.name ?? 'Post media',
|
||||
|
@ -79,8 +79,9 @@ class CloudImageWidget extends ConsumerWidget {
|
||||
static ImageProvider provider({
|
||||
required String fileId,
|
||||
required String serverUrl,
|
||||
bool original = false,
|
||||
}) {
|
||||
final uri = '$serverUrl/files/$fileId';
|
||||
final uri = '$serverUrl/files/$fileId?original=$original';
|
||||
return CachedNetworkImageProvider(uri);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ class PostItem extends HookConsumerWidget {
|
||||
final SnPost item;
|
||||
final EdgeInsets? padding;
|
||||
final bool isOpenable;
|
||||
final bool showReferencePost;
|
||||
final Function? onRefresh;
|
||||
final Function(SnPost)? onUpdate;
|
||||
const PostItem({
|
||||
@ -33,6 +34,7 @@ class PostItem extends HookConsumerWidget {
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
this.isOpenable = true,
|
||||
this.showReferencePost = true,
|
||||
this.onRefresh,
|
||||
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,
|
||||
children: [
|
||||
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)
|
||||
MarkdownTextContent(content: item.content!),
|
||||
if ((item.repliedPost != null ||
|
||||
item.forwardedPost != null) &&
|
||||
showReferencePost)
|
||||
_buildReferencePost(context, item),
|
||||
if (item.attachments.isNotEmpty)
|
||||
CloudFileList(
|
||||
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 {
|
||||
final String parentId;
|
||||
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> {
|
||||
static const int _pageSize = 20;
|
||||
|
||||
String? pubName;
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnPost>> build(String? pubName) {
|
||||
this.pubName = pubName;
|
||||
|
@ -6,7 +6,7 @@ part of 'post_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$postListNotifierHash() => r'6568b7a5afad71551009d9bc7af26afb4b07c9e5';
|
||||
String _$postListNotifierHash() => r'58a2d5d9a8f742f0a3a3e224a51a811d43903e0d';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@ -94,6 +94,7 @@ class PostRepliesList extends HookConsumerWidget {
|
||||
PostItem(
|
||||
item: data.items[index],
|
||||
backgroundColor: isWide ? Colors.transparent : null,
|
||||
showReferencePost: false,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
|
@ -15,6 +15,7 @@
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <record_linux/record_linux_plugin.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <super_native_extensions/super_native_extensions_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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||
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_video
|
||||
pasteboard
|
||||
record_linux
|
||||
sqlite3_flutter_libs
|
||||
super_native_extensions
|
||||
url_launcher_linux
|
||||
|
@ -23,6 +23,7 @@ import media_kit_video
|
||||
import package_info_plus
|
||||
import pasteboard
|
||||
import path_provider_foundation
|
||||
import record_macos
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import sqlite3_flutter_libs
|
||||
@ -50,6 +51,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||
|
@ -107,6 +107,8 @@ PODS:
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- record_macos (1.0.0):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
@ -167,6 +169,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
@ -232,6 +235,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
record_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite_darwin:
|
||||
@ -278,6 +283,7 @@ SPEC CHECKSUMS:
|
||||
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
|
@ -7,7 +7,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<string>Solian</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@ -15,7 +15,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<string>Solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
74
pubspec.lock
74
pubspec.lock
@ -917,6 +917,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
gal:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: gal
|
||||
sha256: "1bdef5879e4569910cfd8c77f460f98fcb7a1f910026af1daa80869856c67d66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
gap:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1550,6 +1558,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1977,7 +2049,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "55fd380bcca8c984773711062ac7dfdbfa87c9d1"
|
||||
resolved-ref: "55e0eecfb7a7af67be4a7b6e8e73d128d4460436"
|
||||
url: "https://github.com/LittleSheep2Code/tus_client.git"
|
||||
source: git
|
||||
version: "2.5.0"
|
||||
|
@ -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: 3.0.0+100
|
||||
version: 3.0.0+103
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
@ -98,12 +98,14 @@ dependencies:
|
||||
visibility_detector: ^0.4.0+2
|
||||
flutter_native_splash: ^2.4.6
|
||||
photo_view: ^0.15.0
|
||||
gal: ^1.9.1
|
||||
dismissible_page: ^1.0.2
|
||||
super_sliver_list: ^0.4.1
|
||||
flutter_webrtc: ^0.14.1
|
||||
livekit_client: ^2.4.7
|
||||
pasteboard: ^0.4.0
|
||||
flutter_colorpicker: ^1.1.0
|
||||
record: ^6.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -19,6 +19,7 @@
|
||||
#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 <pasteboard/pasteboard_plugin.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
@ -51,6 +52,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||
PasteboardPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
|
||||
|
@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
pasteboard
|
||||
record_windows
|
||||
sqlite3_flutter_libs
|
||||
super_native_extensions
|
||||
url_launcher_windows
|
||||
|
Reference in New Issue
Block a user