Compare commits
28 Commits
f542d9fa97
...
3.3.0+145
| Author | SHA1 | Date | |
|---|---|---|---|
|
5ebefae961
|
|||
|
d4758674bb
|
|||
|
f5f1ddc0ea
|
|||
|
2720b59485
|
|||
|
29b1ac7fce
|
|||
|
83ca5551ad
|
|||
| 611cb024a9 | |||
|
74fb56891d
|
|||
|
ac4fa5eb85
|
|||
|
8857718709
|
|||
|
dd17b2b9c1
|
|||
|
848439f664
|
|||
|
f83117424d
|
|||
|
8c19c32c76
|
|||
|
d62b2bed80
|
|||
|
5a23eb1768
|
|||
|
5f6e4763d3
|
|||
|
580c36fb89
|
|||
|
6c25af3b30
|
|||
|
a1da72d447
|
|||
|
ab4120cc22
|
|||
|
52eff0fa25
|
|||
|
beeb28abf2
|
|||
|
c0ab3837ac
|
|||
|
59d38c0d8d
|
|||
|
bd2247ce86
|
|||
|
da2d3f7f17
|
|||
|
7497b77384
|
@@ -43,6 +43,16 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- App protocol -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||
<data android:scheme="solian" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deeplinking -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
"accountConnectionProviderDiscord": "Discord",
|
||||
"accountConnectionProviderAfdian": "Afdian",
|
||||
"accountConnectionProviderSpotify": "Spotify",
|
||||
"accountConnectionProviderSteam": "Steam",
|
||||
"checkIn": "Check In",
|
||||
"checkInNone": "Not checked-in yet",
|
||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||
@@ -1309,5 +1310,15 @@
|
||||
"presenceTypeGaming": "Playing",
|
||||
"presenceTypeMusic": "Listening to Music",
|
||||
"presenceTypeWorkout": "Working out",
|
||||
"articleCompose": "Compose Article"
|
||||
"articleCompose": "Compose Article",
|
||||
"backToHub": "Back to Hub",
|
||||
"advancedFilters": "Advanced Filters",
|
||||
"searchPosts": "Search Posts",
|
||||
"sortBy": "Sort by",
|
||||
"fromDate": "From Date",
|
||||
"toDate": "To Date",
|
||||
"popularity": "Popularity",
|
||||
"descendingOrder": "Descending Order",
|
||||
"selectDate": "Select Date",
|
||||
"pinnedPosts": "Pinned Posts"
|
||||
}
|
||||
|
||||
@@ -158,11 +158,11 @@
|
||||
"checkIn": "签到",
|
||||
"checkInNone": "尚未签到",
|
||||
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
||||
"checkInResultLevel0": "最差运气",
|
||||
"checkInResultLevel1": "坏运气",
|
||||
"checkInResultLevel2": "一个普通的日常",
|
||||
"checkInResultLevel3": "好运",
|
||||
"checkInResultLevel4": "最佳运气",
|
||||
"checkInResultLevel0": "大凶",
|
||||
"checkInResultLevel1": "凶",
|
||||
"checkInResultLevel2": "中平",
|
||||
"checkInResultLevel3": "吉",
|
||||
"checkInResultLevel4": "大吉",
|
||||
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
||||
"eventCalander": "活动日历",
|
||||
"eventCalanderEmpty": "该日无活动。",
|
||||
|
||||
BIN
assets/icons/icon-tray.png
Normal file
BIN
assets/icons/icon-tray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
1
assets/images/oidc/steam.svg
Normal file
1
assets/images/oidc/steam.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="2471" height="2500" viewBox="0 0 256 259" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M127.779 0C60.42 0 5.24 52.412 0 119.014l68.724 28.674a35.812 35.812 0 0 1 20.426-6.366c.682 0 1.356.019 2.02.056l30.566-44.71v-.626c0-26.903 21.69-48.796 48.353-48.796 26.662 0 48.352 21.893 48.352 48.796 0 26.902-21.69 48.804-48.352 48.804-.37 0-.73-.009-1.098-.018l-43.593 31.377c.028.582.046 1.163.046 1.735 0 20.204-16.283 36.636-36.294 36.636-17.566 0-32.263-12.658-35.584-29.412L4.41 164.654c15.223 54.313 64.673 94.132 123.369 94.132 70.818 0 128.221-57.938 128.221-129.393C256 57.93 198.597 0 127.779 0zM80.352 196.332l-15.749-6.568c2.787 5.867 7.621 10.775 14.033 13.47 13.857 5.83 29.836-.803 35.612-14.799a27.555 27.555 0 0 0 .046-21.035c-2.768-6.79-7.999-12.086-14.706-14.909-6.67-2.795-13.811-2.694-20.085-.304l16.275 6.79c10.222 4.3 15.056 16.145 10.794 26.46-4.253 10.314-15.998 15.195-26.22 10.895zm121.957-100.29c0-17.925-14.457-32.52-32.217-32.52-17.769 0-32.226 14.595-32.226 32.52 0 17.926 14.457 32.512 32.226 32.512 17.76 0 32.217-14.586 32.217-32.512zm-56.37-.055c0-13.488 10.84-24.42 24.2-24.42 13.368 0 24.208 10.932 24.208 24.42 0 13.488-10.84 24.421-24.209 24.421-13.359 0-24.2-10.933-24.2-24.42z" fill="#1A1918"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,13 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>BUNDLE_ID</key>
|
||||
<string>dev.solsynth.solian</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -38,7 +38,9 @@
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solian</string>
|
||||
@@ -50,15 +52,16 @@
|
||||
<key>CLIENT_ID</key>
|
||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue
|
||||
your action quickly.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
@@ -75,7 +78,7 @@
|
||||
<key>REVERSED_CLIENT_ID</key>
|
||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
@@ -88,7 +91,7 @@
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
@@ -104,5 +107,5 @@
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -9,6 +9,7 @@ import UserNotifications
|
||||
import Intents
|
||||
import Kingfisher
|
||||
import UniformTypeIdentifiers
|
||||
import KingfisherWebP
|
||||
|
||||
enum ParseNotificationPayloadError: Error {
|
||||
case missingMetadata(String)
|
||||
@@ -24,6 +25,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
_ request: UNNotificationRequest,
|
||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
||||
) {
|
||||
KingfisherManager.shared.defaultOptions += [
|
||||
.processor(WebPProcessor.default),
|
||||
.cacheSerializer(WebPSerializer.default)
|
||||
]
|
||||
|
||||
self.contentHandler = contentHandler
|
||||
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
||||
contentHandler(request.content)
|
||||
@@ -64,11 +70,39 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown)
|
||||
|
||||
let completeNotificationProcessing: (Data?) -> Void = { imageData in
|
||||
let sender = INPerson(
|
||||
personHandle: handle,
|
||||
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
|
||||
displayName: content.title,
|
||||
image: imageData == nil ? nil : INImage(imageData: imageData!),
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil
|
||||
)
|
||||
|
||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
||||
self.donateInteraction(for: intent)
|
||||
|
||||
if let updatedContent = try? request.content.updating(from: intent) {
|
||||
if let mutableContent = updatedContent.mutableCopy() as? UNMutableNotificationContent {
|
||||
mutableContent.categoryIdentifier = "CHAT_MESSAGE"
|
||||
self.contentHandler?(mutableContent)
|
||||
} else {
|
||||
self.contentHandler?(updatedContent)
|
||||
}
|
||||
} else {
|
||||
content.categoryIdentifier = "CHAT_MESSAGE"
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
}
|
||||
|
||||
if let pfpUrl = pfpUrl, let url = URL(string: pfpUrl) {
|
||||
let targetSize = 512
|
||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||
|
||||
KingfisherManager.shared.retrieveImage(with: url, options: [.processor(scaleProcessor)], completionHandler: { result in
|
||||
KingfisherManager.shared.retrieveImage(with: url, options: [
|
||||
.processor(scaleProcessor)
|
||||
], completionHandler: { result in
|
||||
var image: Data?
|
||||
switch result {
|
||||
case .success(let value):
|
||||
@@ -76,37 +110,10 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
case .failure(let error):
|
||||
print("Unable to get pfp url: \(error)")
|
||||
}
|
||||
|
||||
let sender = INPerson(
|
||||
personHandle: handle,
|
||||
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
|
||||
displayName: content.title,
|
||||
image: image == nil ? nil : INImage(imageData: image!),
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil
|
||||
)
|
||||
|
||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
||||
self.donateInteraction(for: intent)
|
||||
|
||||
content.categoryIdentifier = "CHAT_MESSAGE"
|
||||
self.contentHandler?(content)
|
||||
completeNotificationProcessing(image)
|
||||
})
|
||||
} else {
|
||||
let sender = INPerson(
|
||||
personHandle: handle,
|
||||
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
|
||||
displayName: content.title,
|
||||
image: nil,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil
|
||||
)
|
||||
|
||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
||||
self.donateInteraction(for: intent)
|
||||
|
||||
content.categoryIdentifier = "CHAT_MESSAGE"
|
||||
self.contentHandler?(content)
|
||||
completeNotificationProcessing(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import 'package:talker_flutter/talker_flutter.dart';
|
||||
import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
@@ -50,6 +51,12 @@ void main() async {
|
||||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||||
}
|
||||
|
||||
if (!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows)) {
|
||||
talker.info("[SplashScreen] Initializing desktop window manager...");
|
||||
await protocolHandler.register('myprotocol');
|
||||
talker.info("[SplashScreen] Desktop window manager is ready!");
|
||||
}
|
||||
|
||||
try {
|
||||
await EasyLocalization.ensureInitialized();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ part 'poll.g.dart';
|
||||
@freezed
|
||||
sealed class SnPollWithStats with _$SnPollWithStats {
|
||||
const factory SnPollWithStats({
|
||||
required Map<String, dynamic>? userAnswer,
|
||||
required SnPollAnswer? userAnswer,
|
||||
@Default({}) Map<String, dynamic> stats,
|
||||
required String id,
|
||||
required List<SnPollQuestion> questions,
|
||||
|
||||
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$SnPollWithStats {
|
||||
|
||||
Map<String, dynamic>? get userAnswer; Map<String, dynamic> get stats; String get id; List<SnPollQuestion> get questions; String? get title; String? get description; DateTime? get endedAt; String get publisherId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
SnPollAnswer? get userAnswer; Map<String, dynamic> get stats; String get id; List<SnPollQuestion> get questions; String? get title; String? get description; DateTime? get endedAt; String get publisherId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnPollWithStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -28,12 +28,12 @@ $SnPollWithStatsCopyWith<SnPollWithStats> get copyWith => _$SnPollWithStatsCopyW
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPollWithStats&&const DeepCollectionEquality().equals(other.userAnswer, userAnswer)&&const DeepCollectionEquality().equals(other.stats, stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.questions, questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPollWithStats&&(identical(other.userAnswer, userAnswer) || other.userAnswer == userAnswer)&&const DeepCollectionEquality().equals(other.stats, stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.questions, questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(userAnswer),const DeepCollectionEquality().hash(stats),id,const DeepCollectionEquality().hash(questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,userAnswer,const DeepCollectionEquality().hash(stats),id,const DeepCollectionEquality().hash(questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -48,11 +48,11 @@ abstract mixin class $SnPollWithStatsCopyWith<$Res> {
|
||||
factory $SnPollWithStatsCopyWith(SnPollWithStats value, $Res Function(SnPollWithStats) _then) = _$SnPollWithStatsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
$SnPollAnswerCopyWith<$Res>? get userAnswer;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -68,7 +68,7 @@ class _$SnPollWithStatsCopyWithImpl<$Res>
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? userAnswer = freezed,Object? stats = null,Object? id = null,Object? questions = null,Object? title = freezed,Object? description = freezed,Object? endedAt = freezed,Object? publisherId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
userAnswer: freezed == userAnswer ? _self.userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable
|
||||
as SnPollAnswer?,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,questions: null == questions ? _self.questions : questions // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPollQuestion>,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
@@ -81,7 +81,19 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnPollWithStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollAnswerCopyWith<$Res>? get userAnswer {
|
||||
if (_self.userAnswer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPollAnswerCopyWith<$Res>(_self.userAnswer!, (value) {
|
||||
return _then(_self.copyWith(userAnswer: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +172,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPollWithStats() when $default != null:
|
||||
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -181,7 +193,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPollWithStats():
|
||||
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
@@ -198,7 +210,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPollWithStats() when $default != null:
|
||||
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -213,18 +225,10 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnPollWithStats implements SnPollWithStats {
|
||||
const _SnPollWithStats({required final Map<String, dynamic>? userAnswer, final Map<String, dynamic> stats = const {}, required this.id, required final List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions;
|
||||
const _SnPollWithStats({required this.userAnswer, final Map<String, dynamic> stats = const {}, required this.id, required final List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _stats = stats,_questions = questions;
|
||||
factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
|
||||
|
||||
final Map<String, dynamic>? _userAnswer;
|
||||
@override Map<String, dynamic>? get userAnswer {
|
||||
final value = _userAnswer;
|
||||
if (value == null) return null;
|
||||
if (_userAnswer is EqualUnmodifiableMapView) return _userAnswer;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final SnPollAnswer? userAnswer;
|
||||
final Map<String, dynamic> _stats;
|
||||
@override@JsonKey() Map<String, dynamic> get stats {
|
||||
if (_stats is EqualUnmodifiableMapView) return _stats;
|
||||
@@ -261,12 +265,12 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPollWithStats&&const DeepCollectionEquality().equals(other._userAnswer, _userAnswer)&&const DeepCollectionEquality().equals(other._stats, _stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._questions, _questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPollWithStats&&(identical(other.userAnswer, userAnswer) || other.userAnswer == userAnswer)&&const DeepCollectionEquality().equals(other._stats, _stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._questions, _questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_userAnswer),const DeepCollectionEquality().hash(_stats),id,const DeepCollectionEquality().hash(_questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,userAnswer,const DeepCollectionEquality().hash(_stats),id,const DeepCollectionEquality().hash(_questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -281,11 +285,11 @@ abstract mixin class _$SnPollWithStatsCopyWith<$Res> implements $SnPollWithStats
|
||||
factory _$SnPollWithStatsCopyWith(_SnPollWithStats value, $Res Function(_SnPollWithStats) _then) = __$SnPollWithStatsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
@override $SnPollAnswerCopyWith<$Res>? get userAnswer;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -300,8 +304,8 @@ class __$SnPollWithStatsCopyWithImpl<$Res>
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? userAnswer = freezed,Object? stats = null,Object? id = null,Object? questions = null,Object? title = freezed,Object? description = freezed,Object? endedAt = freezed,Object? publisherId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnPollWithStats(
|
||||
userAnswer: freezed == userAnswer ? _self._userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,stats: null == stats ? _self._stats : stats // ignore: cast_nullable_to_non_nullable
|
||||
userAnswer: freezed == userAnswer ? _self.userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as SnPollAnswer?,stats: null == stats ? _self._stats : stats // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,questions: null == questions ? _self._questions : questions // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPollQuestion>,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
@@ -315,7 +319,19 @@ as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnPollWithStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollAnswerCopyWith<$Res>? get userAnswer {
|
||||
if (_self.userAnswer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPollAnswerCopyWith<$Res>(_self.userAnswer!, (value) {
|
||||
return _then(_self.copyWith(userAnswer: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,12 @@ part of 'poll.dart';
|
||||
|
||||
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
||||
_SnPollWithStats(
|
||||
userAnswer: json['user_answer'] as Map<String, dynamic>?,
|
||||
userAnswer:
|
||||
json['user_answer'] == null
|
||||
? null
|
||||
: SnPollAnswer.fromJson(
|
||||
json['user_answer'] as Map<String, dynamic>,
|
||||
),
|
||||
stats: json['stats'] as Map<String, dynamic>? ?? const {},
|
||||
id: json['id'] as String,
|
||||
questions:
|
||||
@@ -32,7 +37,7 @@ _SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
||||
|
||||
Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
|
||||
<String, dynamic>{
|
||||
'user_answer': instance.userAnswer,
|
||||
'user_answer': instance.userAnswer?.toJson(),
|
||||
'stats': instance.stats,
|
||||
'id': instance.id,
|
||||
'questions': instance.questions.map((e) => e.toJson()).toList(),
|
||||
|
||||
@@ -120,9 +120,11 @@ class ActivityRpcServer {
|
||||
};
|
||||
|
||||
// Set up IPC close handler
|
||||
_ipcServer!.onSocketClose = (socket) {
|
||||
if (!kIsWeb) {
|
||||
(_ipcServer as dynamic).onSocketClose = (socket) {
|
||||
handlers['close']?.call(socket);
|
||||
};
|
||||
}
|
||||
|
||||
await _ipcServer!.start();
|
||||
} catch (e) {
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'activity_rpc.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$presenceActivitiesHash() =>
|
||||
r'dcea3cad01b4010c0087f5281413d83a754c2a17';
|
||||
r'3bfaa638eeb961ecd62a32d6a7760a6a7e7bf6f2';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -8,11 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/screens/about.dart';
|
||||
import 'package:island/screens/developers/app_detail.dart';
|
||||
import 'package:island/screens/developers/bot_detail.dart';
|
||||
import 'package:island/screens/developers/edit_app.dart';
|
||||
import 'package:island/screens/developers/edit_bot.dart';
|
||||
import 'package:island/screens/developers/hub.dart';
|
||||
import 'package:island/screens/developers/new_app.dart';
|
||||
import 'package:island/screens/developers/new_bot.dart';
|
||||
import 'package:island/screens/developers/edit_project.dart';
|
||||
import 'package:island/screens/developers/new_project.dart';
|
||||
import 'package:island/screens/discovery/articles.dart';
|
||||
@@ -570,25 +566,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return const SizedBox.shrink(); // Temporary placeholder
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'developerAppNew',
|
||||
path: 'apps/new',
|
||||
builder:
|
||||
(context, state) => NewCustomAppScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerAppEdit',
|
||||
path: 'apps/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditAppScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerAppDetail',
|
||||
path: 'apps/:appId',
|
||||
@@ -599,15 +576,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
appId: state.pathParameters['appId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotNew',
|
||||
path: 'bots/new',
|
||||
builder:
|
||||
(context, state) => NewBotScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotDetail',
|
||||
path: 'bots/:botId',
|
||||
@@ -618,16 +586,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
botId: state.pathParameters['botId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotEdit',
|
||||
path: 'bots/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditBotScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -30,6 +30,7 @@ Widget getProviderIcon(String provider, {double size = 24, Color? color}) {
|
||||
case 'github':
|
||||
case 'discord':
|
||||
case 'afdian':
|
||||
case 'steam':
|
||||
return SvgPicture.asset(
|
||||
'assets/images/oidc/$providerLower.svg',
|
||||
width: size,
|
||||
@@ -64,6 +65,8 @@ String getLocalizedProviderName(String provider) {
|
||||
return 'accountConnectionProviderAfdian'.tr();
|
||||
case 'spotify':
|
||||
return 'accountConnectionProviderSpotify'.tr();
|
||||
case 'steam':
|
||||
return 'accountConnectionProviderSteam'.tr();
|
||||
default:
|
||||
return provider;
|
||||
}
|
||||
@@ -164,6 +167,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
'discord',
|
||||
'afdian',
|
||||
'spotify',
|
||||
'steam',
|
||||
];
|
||||
|
||||
Future<void> addConnection() async {
|
||||
@@ -199,12 +203,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
case 'microsoft':
|
||||
case 'google':
|
||||
case 'github':
|
||||
case 'discord':
|
||||
case 'afdian':
|
||||
case 'spotify':
|
||||
default:
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final accessToken = ref.watch(tokenProvider);
|
||||
launchUrlString(
|
||||
@@ -212,9 +211,6 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
);
|
||||
if (context.mounted) Navigator.pop(context, true);
|
||||
break;
|
||||
default:
|
||||
showSnackBar('accountConnectionAddError'.tr());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "dart:async";
|
||||
import "dart:math" as math;
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:file_picker/file_picker.dart";
|
||||
import "package:flutter/material.dart";
|
||||
@@ -140,6 +141,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final messageController = useTextEditingController();
|
||||
final scrollController = useScrollController();
|
||||
|
||||
// Scroll animation notifiers
|
||||
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||
|
||||
final messageReplyingTo = useState<SnChatMessage?>(null);
|
||||
final messageForwardingTo = useState<SnChatMessage?>(null);
|
||||
final messageEditingTo = useState<SnChatMessage?>(null);
|
||||
@@ -164,6 +168,12 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
isLoading = true;
|
||||
messagesNotifier.loadMore().then((_) => isLoading = false);
|
||||
}
|
||||
|
||||
// Update gradient animations
|
||||
final pixels = scrollController.position.pixels;
|
||||
|
||||
// Bottom gradient: appears when not at bottom (pixels > 0)
|
||||
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
scrollController.addListener(onScroll);
|
||||
@@ -589,7 +599,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
listController: listController,
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
80, // Leave space for chat input
|
||||
),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
@@ -828,7 +840,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Messages and Input in Column
|
||||
// Messages only in Column
|
||||
Positioned.fill(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -872,73 +884,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isSelectionMode.value)
|
||||
chatRoom.when(
|
||||
data:
|
||||
(room) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ChatInput(
|
||||
messageController: messageController,
|
||||
chatRoom: room!,
|
||||
onSend: sendMessage,
|
||||
onClear: () {
|
||||
if (messageEditingTo.value != null) {
|
||||
attachments.value.clear();
|
||||
messageController.clear();
|
||||
}
|
||||
messageEditingTo.value = null;
|
||||
messageReplyingTo.value = null;
|
||||
messageForwardingTo.value = null;
|
||||
},
|
||||
messageEditingTo: messageEditingTo.value,
|
||||
messageReplyingTo: messageReplyingTo.value,
|
||||
messageForwardingTo: messageForwardingTo.value,
|
||||
onPickFile: (bool isPhoto) {
|
||||
if (isPhoto) {
|
||||
pickPhotoMedia();
|
||||
} else {
|
||||
pickVideoMedia();
|
||||
}
|
||||
},
|
||||
onPickAudio: pickAudioMedia,
|
||||
onPickGeneralFile: pickGeneralFile,
|
||||
onLinkAttachment: linkAttachment,
|
||||
attachments: attachments.value,
|
||||
onUploadAttachment: uploadAttachment,
|
||||
onDeleteAttachment: (index) async {
|
||||
final attachment = attachments.value[index];
|
||||
if (attachment.isOnCloud &&
|
||||
!attachment.isLink) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.delete(
|
||||
'/drive/files/${attachment.data.id}',
|
||||
);
|
||||
}
|
||||
final clone = List.of(attachments.value);
|
||||
clone.removeAt(index);
|
||||
attachments.value = clone;
|
||||
},
|
||||
onMoveAttachment: (idx, delta) {
|
||||
if (idx + delta < 0 ||
|
||||
idx + delta >= attachments.value.length) {
|
||||
return;
|
||||
}
|
||||
final clone = List.of(attachments.value);
|
||||
clone.insert(idx + delta, clone.removeAt(idx));
|
||||
attachments.value = clone;
|
||||
},
|
||||
onAttachmentsChanged: (newAttachments) {
|
||||
attachments.value = newAttachments;
|
||||
},
|
||||
attachmentProgress: attachmentProgress.value,
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -977,6 +922,112 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom gradient - appears when scrolling towards newer messages (behind chat input)
|
||||
if (!isSelectionMode.value)
|
||||
AnimatedBuilder(
|
||||
animation: bottomGradientNotifier.value,
|
||||
builder:
|
||||
(context, child) => Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Opacity(
|
||||
opacity: bottomGradientNotifier.value.value,
|
||||
child: Container(
|
||||
height: math.min(
|
||||
MediaQuery.of(context).size.height * 0.1,
|
||||
128,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer.withOpacity(0.8),
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Chat Input positioned above gradient (higher z-index)
|
||||
if (!isSelectionMode.value)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0, // At the very bottom, above gradient
|
||||
child: chatRoom.when(
|
||||
data:
|
||||
(room) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ChatInput(
|
||||
messageController: messageController,
|
||||
chatRoom: room!,
|
||||
onSend: sendMessage,
|
||||
onClear: () {
|
||||
if (messageEditingTo.value != null) {
|
||||
attachments.value.clear();
|
||||
messageController.clear();
|
||||
}
|
||||
messageEditingTo.value = null;
|
||||
messageReplyingTo.value = null;
|
||||
messageForwardingTo.value = null;
|
||||
},
|
||||
messageEditingTo: messageEditingTo.value,
|
||||
messageReplyingTo: messageReplyingTo.value,
|
||||
messageForwardingTo: messageForwardingTo.value,
|
||||
onPickFile: (bool isPhoto) {
|
||||
if (isPhoto) {
|
||||
pickPhotoMedia();
|
||||
} else {
|
||||
pickVideoMedia();
|
||||
}
|
||||
},
|
||||
onPickAudio: pickAudioMedia,
|
||||
onPickGeneralFile: pickGeneralFile,
|
||||
onLinkAttachment: linkAttachment,
|
||||
attachments: attachments.value,
|
||||
onUploadAttachment: uploadAttachment,
|
||||
onDeleteAttachment: (index) async {
|
||||
final attachment = attachments.value[index];
|
||||
if (attachment.isOnCloud && !attachment.isLink) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.delete(
|
||||
'/drive/files/${attachment.data.id}',
|
||||
);
|
||||
}
|
||||
final clone = List.of(attachments.value);
|
||||
clone.removeAt(index);
|
||||
attachments.value = clone;
|
||||
},
|
||||
onMoveAttachment: (idx, delta) {
|
||||
if (idx + delta < 0 ||
|
||||
idx + delta >= attachments.value.length) {
|
||||
return;
|
||||
}
|
||||
final clone = List.of(attachments.value);
|
||||
clone.insert(idx + delta, clone.removeAt(idx));
|
||||
attachments.value = clone;
|
||||
},
|
||||
onAttachmentsChanged: (newAttachments) {
|
||||
attachments.value = newAttachments;
|
||||
},
|
||||
attachmentProgress: attachmentProgress.value,
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
// Selection mode toolbar
|
||||
if (isSelectionMode.value)
|
||||
Positioned(
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/custom_app.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/developers/edit_app.dart';
|
||||
import 'package:island/screens/developers/new_app.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -68,12 +73,18 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
context.pushNamed(
|
||||
'developerAppNew',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
},
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createCustomApp'.tr(),
|
||||
child: NewCustomAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
@@ -83,18 +94,44 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
return RefreshIndicator(
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
customAppsProvider(publisherName, projectId).future,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
Card(
|
||||
child: ListTile(
|
||||
title: Text('customApps').tr().padding(horizontal: 8),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createCustomApp'.tr(),
|
||||
child: NewCustomAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 4),
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final app = data[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
@@ -138,7 +175,10 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
app.slug,
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
contentPadding: EdgeInsets.only(left: 20, right: 12),
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 12,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
@@ -163,7 +203,9 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
@@ -171,13 +213,19 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
context.pushNamed(
|
||||
'developerAppEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'id': app.id,
|
||||
},
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'editCustomApp'.tr(),
|
||||
child: EditAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
id: app.id,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
@@ -185,7 +233,9 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
'deleteCustomApp'.tr(),
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final client = ref.read(
|
||||
apiClientProvider,
|
||||
);
|
||||
client.delete(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
|
||||
);
|
||||
@@ -207,6 +257,9 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/bot.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/developers/edit_bot.dart';
|
||||
import 'package:island/screens/developers/new_bot.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'bots.g.dart';
|
||||
|
||||
@@ -46,12 +51,18 @@ class BotsScreen extends HookConsumerWidget {
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
context.pushNamed(
|
||||
'developerBotNew',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
},
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createBot'.tr(),
|
||||
child: NewBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
@@ -64,13 +75,39 @@ class BotsScreen extends HookConsumerWidget {
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(botsProvider(publisherName, projectId).future),
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
Card(
|
||||
child: ListTile(
|
||||
title: Text('bots').tr().padding(horizontal: 8),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createBot'.tr(),
|
||||
child: NewBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final bot = data[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
child: ListTile(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
@@ -102,7 +139,10 @@ class BotsScreen extends HookConsumerWidget {
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const Icon(
|
||||
Symbols.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'delete',
|
||||
@@ -114,13 +154,19 @@ class BotsScreen extends HookConsumerWidget {
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
context.pushNamed(
|
||||
'developerBotEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'id': bot.id,
|
||||
},
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'editBot'.tr(),
|
||||
child: EditBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
id: bot.id,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
@@ -154,6 +200,9 @@ class BotsScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
|
||||
@@ -39,11 +39,13 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String? id;
|
||||
final bool isModal;
|
||||
const EditAppScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
this.id,
|
||||
this.isModal = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -177,7 +179,12 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: scopeController,
|
||||
decoration: InputDecoration(labelText: 'scopeName'.tr()),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'scopeName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.tonalIcon(
|
||||
@@ -220,6 +227,9 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
hintText: 'https://example.com/auth/callback',
|
||||
helperText: 'redirectUriHint'.tr(),
|
||||
helperMaxLines: 3,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
@@ -316,12 +326,7 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
||||
),
|
||||
body:
|
||||
final bodyContent =
|
||||
app == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: app?.hasError == true && !isNew
|
||||
@@ -382,11 +387,17 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
@@ -394,11 +405,15 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
@@ -406,12 +421,16 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ExpansionPanelList(
|
||||
@@ -438,6 +457,11 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'homePageUrl'.tr(),
|
||||
hintText: 'https://example.com',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
@@ -446,6 +470,11 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'privacyPolicyUrl'.tr(),
|
||||
hintText: 'https://example.com/privacy',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
@@ -454,6 +483,11 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'termsOfServiceUrl'.tr(),
|
||||
hintText: 'https://example.com/terms',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
@@ -463,9 +497,8 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
),
|
||||
ExpansionPanel(
|
||||
headerBuilder:
|
||||
(context, isExpanded) => ListTile(
|
||||
title: Text('oauthConfig').tr(),
|
||||
),
|
||||
(context, isExpanded) =>
|
||||
ListTile(title: Text('oauthConfig').tr()),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -480,15 +513,11 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
(uri) => ListTile(
|
||||
title: Text(uri),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.delete,
|
||||
),
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
redirectUris.value =
|
||||
redirectUris.value
|
||||
.where(
|
||||
(u) => u != uri,
|
||||
)
|
||||
.where((u) => u != uri)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
@@ -501,8 +530,9 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
title: Text('addRedirectUri'.tr()),
|
||||
onTap: showAddRedirectUriDialog,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -520,9 +550,7 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
(scope) => ListTile(
|
||||
title: Text(scope),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.delete,
|
||||
),
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
allowedScopes.value =
|
||||
allowedScopes.value
|
||||
@@ -568,8 +596,7 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed:
|
||||
submitting.value ? null : performAction,
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges'.tr()),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
@@ -579,7 +606,18 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (isModal) {
|
||||
return bodyContent;
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
||||
),
|
||||
body: bodyContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,13 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String? id;
|
||||
final bool isModal;
|
||||
const EditBotScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
this.id,
|
||||
this.isModal = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -191,10 +193,7 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
||||
body:
|
||||
final bodyContent =
|
||||
botData == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: botData?.hasError == true && !isNew
|
||||
@@ -255,7 +254,14 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
@@ -263,6 +269,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'nickname'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -271,6 +282,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -279,6 +295,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: 'bio'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
@@ -291,6 +312,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
controller: firstNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'firstName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -299,6 +325,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
controller: middleNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'middleName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -307,6 +338,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
controller: lastNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'lastName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -321,6 +357,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
controller: genderController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'gender'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -329,6 +370,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
controller: pronounsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'pronouns'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -343,6 +389,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
controller: locationController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'location'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -351,6 +402,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
controller: timeZoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'timeZone'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -370,13 +426,14 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
@@ -403,8 +460,7 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed:
|
||||
submitting.value ? null : performAction,
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges').tr(),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
@@ -414,7 +470,16 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (isModal) {
|
||||
return bodyContent;
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
||||
body: bodyContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -99,15 +97,6 @@ class DeveloperHubScreen extends HookConsumerWidget {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (currentProject.value == null)
|
||||
...([
|
||||
// Welcome Section
|
||||
_WelcomeSection(currentDeveloper: currentDeveloper.value),
|
||||
|
||||
// Navigation Tabs
|
||||
_NavigationTabs(),
|
||||
]),
|
||||
|
||||
// Main Content
|
||||
if (currentProject.value != null)
|
||||
Expanded(
|
||||
@@ -195,162 +184,6 @@ class _ConsoleAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Welcome Section
|
||||
class _WelcomeSection extends StatelessWidget {
|
||||
final SnDeveloper? currentDeveloper;
|
||||
|
||||
const _WelcomeSection({required this.currentDeveloper});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors:
|
||||
isDark
|
||||
? [
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
]
|
||||
: [const Color(0xFFE8F0FE), const Color(0xFFF1F3F4)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 16,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: _RandomStickerImage(
|
||||
width: 180,
|
||||
height: 180,
|
||||
).opacity(isWideScreen(context) ? 1 : 0.5),
|
||||
),
|
||||
Container(
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Good morning!',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
currentDeveloper != null
|
||||
? "You're working as ${currentDeveloper!.publisher!.nick}"
|
||||
: "Choose a developer and continue.",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Random Sticker Image Widget
|
||||
class _RandomStickerImage extends StatelessWidget {
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const _RandomStickerImage({this.width, this.height});
|
||||
|
||||
static const List<String> _stickers = [
|
||||
'assets/images/stickers/clap.png',
|
||||
'assets/images/stickers/confuse.png',
|
||||
'assets/images/stickers/pray.png',
|
||||
'assets/images/stickers/thumb_up.png',
|
||||
];
|
||||
|
||||
String _getRandomSticker() {
|
||||
final random = Random();
|
||||
return _stickers[random.nextInt(_stickers.length)];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Image.asset(
|
||||
_getRandomSticker(),
|
||||
width: width ?? 80,
|
||||
height: height ?? 80,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation Tabs
|
||||
class _NavigationTabs extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface),
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(24),
|
||||
_NavTabItem(title: 'Dashboard', isActive: true),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavTabItem extends StatelessWidget {
|
||||
final String title;
|
||||
final bool isActive;
|
||||
|
||||
const _NavTabItem({required this.title, this.isActive = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color:
|
||||
isActive
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isActive
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: isActive ? FontWeight.w500 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Main Content Section
|
||||
class _MainContentSection extends HookConsumerWidget {
|
||||
final SnDeveloper? currentDeveloper;
|
||||
|
||||
@@ -4,10 +4,20 @@ import 'package:island/screens/developers/edit_app.dart';
|
||||
class NewCustomAppScreen extends StatelessWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
const NewCustomAppScreen({super.key, required this.publisherName, required this.projectId});
|
||||
final bool isModal;
|
||||
const NewCustomAppScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
this.isModal = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditAppScreen(publisherName: publisherName, projectId: projectId);
|
||||
return EditAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: isModal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/screens/developers/edit_bot.dart';
|
||||
|
||||
class NewBotScreen extends StatelessWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
const NewBotScreen({super.key, required this.publisherName, required this.projectId});
|
||||
final bool isModal;
|
||||
const NewBotScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
this.isModal = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditBotScreen(publisherName: publisherName, projectId: projectId);
|
||||
return EditBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: isModal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/dev_project.dart';
|
||||
import 'package:island/screens/developers/apps.dart';
|
||||
import 'package:island/screens/developers/bots.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class ProjectDetailView extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
@@ -54,6 +56,37 @@ class ProjectDetailView extends HookConsumerWidget {
|
||||
label: Text('bots'.tr()),
|
||||
),
|
||||
],
|
||||
leading: Container(
|
||||
width: isWiderScreen(context) ? 256 : 80,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
bottom: 8,
|
||||
top: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: onBackToHub,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
iconSize: 16,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
if (isWiderScreen(context))
|
||||
Expanded(child: Text("backToHub").tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -69,6 +102,7 @@ class ProjectDetailView extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
@@ -97,7 +131,7 @@ class ProjectDetailView extends HookConsumerWidget {
|
||||
),
|
||||
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||
],
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -737,7 +737,7 @@ class ActivityListNotifier extends _$ActivityListNotifier
|
||||
};
|
||||
|
||||
final response = await client.get(
|
||||
'/sphere/activities',
|
||||
'/sphere/timeline',
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'explore.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$activityListNotifierHash() =>
|
||||
r'a3ad3242f08139bef14a2f0fab6591ce8b3cb9f0';
|
||||
r'77ffc7852feffa5438b56fa26123d453b7c310cf';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -201,7 +201,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.add_circle,
|
||||
Symbols.remove_circle,
|
||||
),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
@@ -214,7 +214,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.remove_circle,
|
||||
Symbols.add_circle,
|
||||
),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
|
||||
@@ -326,14 +326,34 @@ class _PublisherHeatmapWidget extends StatelessWidget {
|
||||
|
||||
class _PublisherCategoryTabWidget extends StatelessWidget {
|
||||
final TabController categoryTabController;
|
||||
final ValueNotifier<bool?> includeReplies;
|
||||
final ValueNotifier<bool> mediaOnly;
|
||||
final ValueNotifier<String?> queryTerm;
|
||||
final ValueNotifier<String?> order;
|
||||
final ValueNotifier<bool> orderDesc;
|
||||
final ValueNotifier<int?> periodStart;
|
||||
final ValueNotifier<int?> periodEnd;
|
||||
final ValueNotifier<bool> showAdvancedFilters;
|
||||
|
||||
const _PublisherCategoryTabWidget({required this.categoryTabController});
|
||||
const _PublisherCategoryTabWidget({
|
||||
required this.categoryTabController,
|
||||
required this.includeReplies,
|
||||
required this.mediaOnly,
|
||||
required this.queryTerm,
|
||||
required this.order,
|
||||
required this.orderDesc,
|
||||
required this.periodStart,
|
||||
required this.periodEnd,
|
||||
required this.showAdvancedFilters,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: TabBar(
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: categoryTabController,
|
||||
dividerColor: Colors.transparent,
|
||||
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
@@ -343,6 +363,228 @@ class _PublisherCategoryTabWidget extends StatelessWidget {
|
||||
Tab(text: 'postArticle'.tr()),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
title: Text('reply'.tr()),
|
||||
value: includeReplies.value,
|
||||
tristate: true,
|
||||
onChanged: (value) {
|
||||
// Cycle through: null -> false -> true -> null
|
||||
if (includeReplies.value == null) {
|
||||
includeReplies.value = false;
|
||||
} else if (includeReplies.value == false) {
|
||||
includeReplies.value = true;
|
||||
} else {
|
||||
includeReplies.value = null;
|
||||
}
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
secondary: const Icon(Symbols.reply),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
title: Text('attachments'.tr()),
|
||||
value: mediaOnly.value,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
mediaOnly.value = value;
|
||||
}
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
secondary: const Icon(Symbols.attachment),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text('descendingOrder'.tr()),
|
||||
value: orderDesc.value,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
orderDesc.value = value;
|
||||
}
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
secondary: const Icon(Symbols.sort),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text('advancedFilters'.tr()),
|
||||
leading: const Icon(Symbols.filter_list),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(const Radius.circular(8)),
|
||||
),
|
||||
trailing: Icon(
|
||||
showAdvancedFilters.value
|
||||
? Symbols.expand_less
|
||||
: Symbols.expand_more,
|
||||
),
|
||||
onTap: () {
|
||||
showAdvancedFilters.value = !showAdvancedFilters.value;
|
||||
},
|
||||
),
|
||||
if (showAdvancedFilters.value) ...[
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'search'.tr(),
|
||||
hintText: 'searchPosts'.tr(),
|
||||
prefixIcon: const Icon(Symbols.search),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
queryTerm.value = value.isEmpty ? null : value;
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'sortBy'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
value: order.value,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
|
||||
DropdownMenuItem(
|
||||
value: 'popularity',
|
||||
child: Text('popularity'.tr()),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
order.value = value;
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate:
|
||||
periodStart.value != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
periodStart.value! * 1000,
|
||||
)
|
||||
: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime.now().add(
|
||||
const Duration(days: 365),
|
||||
),
|
||||
);
|
||||
if (pickedDate != null) {
|
||||
periodStart.value =
|
||||
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fromDate'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
suffixIcon: const Icon(Symbols.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
periodStart.value != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
periodStart.value! * 1000,
|
||||
).toString().split(' ')[0]
|
||||
: 'selectDate'.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate:
|
||||
periodEnd.value != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
periodEnd.value! * 1000,
|
||||
)
|
||||
: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime.now().add(
|
||||
const Duration(days: 365),
|
||||
),
|
||||
);
|
||||
if (pickedDate != null) {
|
||||
periodEnd.value =
|
||||
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'toDate'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
suffixIcon: const Icon(Symbols.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
periodEnd.value != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
periodEnd.value! * 1000,
|
||||
).toString().split(' ')[0]
|
||||
: 'selectDate'.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -423,7 +665,18 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
||||
categoryTab.value = categoryTabController.index;
|
||||
});
|
||||
|
||||
final includeReplies = useState<bool?>(null);
|
||||
final mediaOnly = useState(false);
|
||||
final queryTerm = useState<String?>(null);
|
||||
final order = useState<String?>('date'); // 'popularity' or 'date'
|
||||
final orderDesc = useState(
|
||||
true,
|
||||
); // true for descending, false for ascending
|
||||
final periodStart = useState<int?>(null);
|
||||
final periodEnd = useState<int?>(null);
|
||||
final showAdvancedFilters = useState(false);
|
||||
final subscribing = useState(false);
|
||||
final isPinnedExpanded = useState(true);
|
||||
|
||||
Future<void> subscribe() async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
@@ -494,21 +747,66 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverGap(16),
|
||||
SliverToBoxAdapter(
|
||||
child: Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text('pinnedPosts'.tr()),
|
||||
leading: const Icon(Symbols.push_pin),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
isPinnedExpanded.value
|
||||
? Symbols.expand_less
|
||||
: Symbols.expand_more,
|
||||
),
|
||||
onTap:
|
||||
() =>
|
||||
isPinnedExpanded.value =
|
||||
!isPinnedExpanded.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
...[
|
||||
if (isPinnedExpanded.value)
|
||||
SliverPostList(pubName: name, pinned: true),
|
||||
],
|
||||
SliverToBoxAdapter(
|
||||
child: _PublisherCategoryTabWidget(
|
||||
categoryTabController: categoryTabController,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
orderDesc: orderDesc,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
showAdvancedFilters: showAdvancedFilters,
|
||||
),
|
||||
),
|
||||
SliverPostList(
|
||||
key: ValueKey(categoryTab.value),
|
||||
key: ValueKey(
|
||||
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
||||
),
|
||||
pubName: name,
|
||||
pinned: false,
|
||||
type: switch (categoryTab.value) {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
_ => null,
|
||||
},
|
||||
type:
|
||||
categoryTab.value == 1
|
||||
? 0
|
||||
: (categoryTab.value == 2 ? 1 : null),
|
||||
includeReplies: includeReplies.value,
|
||||
mediaOnly: mediaOnly.value,
|
||||
queryTerm: queryTerm.value,
|
||||
order: order.value,
|
||||
orderDesc: orderDesc.value,
|
||||
periodStart: periodStart.value,
|
||||
periodEnd: periodEnd.value,
|
||||
),
|
||||
SliverGap(
|
||||
MediaQuery.of(context).padding.bottom + 16,
|
||||
@@ -617,21 +915,60 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
||||
heatmap: heatmap,
|
||||
).padding(vertical: 4),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text('pinnedPosts'.tr()),
|
||||
trailing: Icon(
|
||||
isPinnedExpanded.value
|
||||
? Symbols.expand_less
|
||||
: Symbols.expand_more,
|
||||
),
|
||||
onTap:
|
||||
() =>
|
||||
isPinnedExpanded.value =
|
||||
!isPinnedExpanded.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
...[
|
||||
if (isPinnedExpanded.value)
|
||||
SliverPostList(pubName: name, pinned: true),
|
||||
],
|
||||
SliverToBoxAdapter(
|
||||
child: _PublisherCategoryTabWidget(
|
||||
categoryTabController: categoryTabController,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
orderDesc: orderDesc,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
showAdvancedFilters: showAdvancedFilters,
|
||||
),
|
||||
),
|
||||
SliverPostList(
|
||||
key: ValueKey(categoryTab.value),
|
||||
key: ValueKey(
|
||||
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
||||
),
|
||||
pubName: name,
|
||||
pinned: false,
|
||||
type: switch (categoryTab.value) {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
_ => null,
|
||||
},
|
||||
type:
|
||||
categoryTab.value == 1
|
||||
? 0
|
||||
: (categoryTab.value == 2 ? 1 : null),
|
||||
includeReplies: includeReplies.value,
|
||||
mediaOnly: mediaOnly.value,
|
||||
queryTerm: queryTerm.value,
|
||||
order: order.value,
|
||||
orderDesc: orderDesc.value,
|
||||
periodStart: periodStart.value,
|
||||
periodEnd: periodEnd.value,
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "dart:convert";
|
||||
import "dart:math" as math;
|
||||
import "package:dio/dio.dart";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/material.dart";
|
||||
@@ -57,6 +58,9 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
|
||||
final listController = useMemoized(() => ListController(), []);
|
||||
|
||||
// Scroll animation notifiers
|
||||
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||
|
||||
// Update local thoughts when provider data changes
|
||||
useEffect(() {
|
||||
thoughts.whenData((data) {
|
||||
@@ -86,6 +90,20 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
return null;
|
||||
}, [localThoughts.value.length, isStreaming.value]);
|
||||
|
||||
// Add scroll listener for gradient animations
|
||||
useEffect(() {
|
||||
void onScroll() {
|
||||
// Update gradient animations
|
||||
final pixels = scrollController.position.pixels;
|
||||
|
||||
// Bottom gradient: appears when not at bottom (pixels > 0)
|
||||
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
scrollController.addListener(onScroll);
|
||||
return () => scrollController.removeListener(onScroll);
|
||||
}, [scrollController]);
|
||||
|
||||
void sendMessage() async {
|
||||
if (messageController.text.trim().isEmpty) return;
|
||||
|
||||
@@ -258,7 +276,10 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Thoughts list
|
||||
Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Column(
|
||||
@@ -269,7 +290,12 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
(thoughtList) => SuperListView.builder(
|
||||
listController: listController,
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
80, // Leave space for thought input
|
||||
),
|
||||
reverse: true,
|
||||
itemCount:
|
||||
localThoughts.value.length +
|
||||
@@ -293,7 +319,8 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
loading:
|
||||
() => const Center(child: CircularProgressIndicator()),
|
||||
() =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
@@ -309,15 +336,61 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
ThoughtInput(
|
||||
messageController: messageController,
|
||||
isStreaming: isStreaming.value,
|
||||
onSend: sendMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
|
||||
AnimatedBuilder(
|
||||
animation: bottomGradientNotifier.value,
|
||||
builder:
|
||||
(context, child) => Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Opacity(
|
||||
opacity: bottomGradientNotifier.value.value,
|
||||
child: Container(
|
||||
height: math.min(
|
||||
MediaQuery.of(context).size.height * 0.1,
|
||||
128,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer.withOpacity(0.8),
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Thought Input positioned above gradient (higher z-index)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0, // At the very bottom, above gradient
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: ThoughtInput(
|
||||
messageController: messageController,
|
||||
isStreaming: isStreaming.value,
|
||||
onSend: sendMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "dart:convert";
|
||||
import "dart:math" as math;
|
||||
import "package:dio/dio.dart";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/material.dart";
|
||||
@@ -54,6 +55,9 @@ class ThoughtSheet extends HookConsumerWidget {
|
||||
|
||||
final listController = useMemoized(() => ListController(), []);
|
||||
|
||||
// Scroll animation notifiers
|
||||
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||
|
||||
// Scroll to bottom when thoughts change or streaming state changes
|
||||
useEffect(() {
|
||||
if (localThoughts.value.isNotEmpty || isStreaming.value) {
|
||||
@@ -68,6 +72,20 @@ class ThoughtSheet extends HookConsumerWidget {
|
||||
return null;
|
||||
}, [localThoughts.value.length, isStreaming.value]);
|
||||
|
||||
// Add scroll listener for gradient animations
|
||||
useEffect(() {
|
||||
void onScroll() {
|
||||
// Update gradient animations
|
||||
final pixels = scrollController.position.pixels;
|
||||
|
||||
// Bottom gradient: appears when not at bottom (pixels > 0)
|
||||
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
scrollController.addListener(onScroll);
|
||||
return () => scrollController.removeListener(onScroll);
|
||||
}, [scrollController]);
|
||||
|
||||
void sendMessage() async {
|
||||
if (messageController.text.trim().isEmpty) return;
|
||||
|
||||
@@ -196,7 +214,10 @@ class ThoughtSheet extends HookConsumerWidget {
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: currentTopic.value ?? 'aiThought'.tr(),
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Thoughts list
|
||||
Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Column(
|
||||
@@ -205,10 +226,16 @@ class ThoughtSheet extends HookConsumerWidget {
|
||||
child: SuperListView.builder(
|
||||
listController: listController,
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
80, // Leave space for thought input
|
||||
),
|
||||
reverse: true,
|
||||
itemCount:
|
||||
localThoughts.value.length + (isStreaming.value ? 1 : 0),
|
||||
localThoughts.value.length +
|
||||
(isStreaming.value ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (isStreaming.value && index == 0) {
|
||||
return ThoughtItem(
|
||||
@@ -218,7 +245,8 @@ class ThoughtSheet extends HookConsumerWidget {
|
||||
streamingFunctionCalls: functionCalls.value,
|
||||
);
|
||||
}
|
||||
final thoughtIndex = isStreaming.value ? index - 1 : index;
|
||||
final thoughtIndex =
|
||||
isStreaming.value ? index - 1 : index;
|
||||
final thought = localThoughts.value[thoughtIndex];
|
||||
return ThoughtItem(
|
||||
thought: thought,
|
||||
@@ -227,17 +255,63 @@ class ThoughtSheet extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
ThoughtInput(
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
|
||||
AnimatedBuilder(
|
||||
animation: bottomGradientNotifier.value,
|
||||
builder:
|
||||
(context, child) => Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Opacity(
|
||||
opacity: bottomGradientNotifier.value.value,
|
||||
child: Container(
|
||||
height: math.min(
|
||||
MediaQuery.of(context).size.height * 0.1,
|
||||
128,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer.withOpacity(0.8),
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Thought Input positioned above gradient (higher z-index)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0, // At the very bottom, above gradient
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: ThoughtInput(
|
||||
messageController: messageController,
|
||||
isStreaming: isStreaming.value,
|
||||
onSend: sendMessage,
|
||||
attachedMessages: attachedMessages,
|
||||
attachedPosts: attachedPosts,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ class TrayService {
|
||||
await trayManager.setIcon(
|
||||
Platform.isWindows
|
||||
? 'assets/icons/icon.ico'
|
||||
: 'assets/icons/icon-outline.svg',
|
||||
: 'assets/icons/icon-tray.png',
|
||||
isTemplate: Platform.isMacOS,
|
||||
);
|
||||
|
||||
final menu = Menu(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
@@ -16,16 +15,15 @@ class FileUploader {
|
||||
|
||||
FileUploader(this._client);
|
||||
|
||||
/// Calculates the MD5 hash of a file.
|
||||
Future<String> _calculateFileHash(XFile file) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
/// Calculates the MD5 hash of file bytes.
|
||||
String _calculateFileHash(Uint8List bytes) {
|
||||
final digest = md5.convert(bytes);
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
/// Creates an upload task for the given file.
|
||||
Future<Map<String, dynamic>> createUploadTask({
|
||||
required XFile file,
|
||||
required Uint8List bytes,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
@@ -34,8 +32,8 @@ class FileUploader {
|
||||
String? expiredAt,
|
||||
int? chunkSize,
|
||||
}) async {
|
||||
final hash = await _calculateFileHash(file);
|
||||
final fileSize = await file.length();
|
||||
final hash = _calculateFileHash(bytes);
|
||||
final fileSize = bytes.length;
|
||||
|
||||
final response = await _client.post(
|
||||
'/drive/files/upload/create',
|
||||
@@ -83,7 +81,7 @@ class FileUploader {
|
||||
|
||||
/// Uploads a file in chunks using the multi-part API.
|
||||
Future<SnCloudFile> uploadFile({
|
||||
required XFile file,
|
||||
required Uint8List bytes,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
@@ -94,7 +92,7 @@ class FileUploader {
|
||||
}) async {
|
||||
// Step 1: Create upload task
|
||||
final createResponse = await createUploadTask(
|
||||
file: file,
|
||||
bytes: bytes,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
poolId: poolId,
|
||||
@@ -114,24 +112,10 @@ class FileUploader {
|
||||
final chunksCount = createResponse['chunks_count'] as int;
|
||||
|
||||
// Step 2: Upload chunks
|
||||
final stream = file.openRead();
|
||||
final chunks = <Uint8List>[];
|
||||
int bytesRead = 0;
|
||||
final buffer = BytesBuilder();
|
||||
|
||||
await for (final chunk in stream) {
|
||||
buffer.add(chunk);
|
||||
bytesRead += chunk.length;
|
||||
|
||||
if (bytesRead >= chunkSize) {
|
||||
chunks.add(buffer.takeBytes());
|
||||
bytesRead = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining bytes as last chunk
|
||||
if (buffer.length > 0) {
|
||||
chunks.add(buffer.takeBytes());
|
||||
for (int i = 0; i < bytes.length; i += chunkSize) {
|
||||
final end = i + chunkSize > bytes.length ? bytes.length : i + chunkSize;
|
||||
chunks.add(Uint8List.fromList(bytes.sublist(i, end)));
|
||||
}
|
||||
|
||||
// Ensure we have the correct number of chunks
|
||||
@@ -225,20 +209,34 @@ class FileUploader {
|
||||
Completer<SnCloudFile?> completer,
|
||||
) {
|
||||
String actualMimetype = getMimeType(fileData);
|
||||
late XFile file;
|
||||
String actualFilename = fileData.displayName ?? 'randomly_file';
|
||||
Uint8List? byteData;
|
||||
Uint8List? bytes;
|
||||
|
||||
// Handle the data based on what's in the UniversalFile
|
||||
final data = fileData.data;
|
||||
|
||||
if (data is XFile) {
|
||||
file = data;
|
||||
actualFilename = fileData.displayName ?? data.name;
|
||||
// Read bytes from XFile
|
||||
data
|
||||
.readAsBytes()
|
||||
.then((readBytes) {
|
||||
_performUpload(
|
||||
bytes: readBytes,
|
||||
fileName: fileData.displayName ?? data.name,
|
||||
contentType: actualMimetype,
|
||||
client: client,
|
||||
poolId: poolId,
|
||||
onProgress: onProgress,
|
||||
completer: completer,
|
||||
);
|
||||
})
|
||||
.catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
return completer;
|
||||
} else if (data is List<int> || data is Uint8List) {
|
||||
byteData = data is List<int> ? Uint8List.fromList(data) : data;
|
||||
bytes = data is List<int> ? Uint8List.fromList(data) : data;
|
||||
actualFilename = fileData.displayName ?? 'uploaded_file';
|
||||
file = XFile.fromData(byteData!, mimeType: actualMimetype);
|
||||
} else if (data is SnCloudFile) {
|
||||
// If the file is already on the cloud, just return it
|
||||
completer.complete(data);
|
||||
@@ -252,15 +250,40 @@ class FileUploader {
|
||||
return completer;
|
||||
}
|
||||
|
||||
if (bytes != null) {
|
||||
_performUpload(
|
||||
bytes: bytes,
|
||||
fileName: actualFilename,
|
||||
contentType: actualMimetype,
|
||||
client: client,
|
||||
poolId: poolId,
|
||||
onProgress: onProgress,
|
||||
completer: completer,
|
||||
);
|
||||
}
|
||||
|
||||
return completer;
|
||||
}
|
||||
|
||||
// Helper method to perform the actual upload
|
||||
static void _performUpload({
|
||||
required Uint8List bytes,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
required Dio client,
|
||||
String? poolId,
|
||||
Function(double progress, Duration estimate)? onProgress,
|
||||
required Completer<SnCloudFile?> completer,
|
||||
}) {
|
||||
final uploader = FileUploader(client);
|
||||
|
||||
// Call progress start
|
||||
onProgress?.call(0.0, Duration.zero);
|
||||
uploader
|
||||
.uploadFile(
|
||||
file: file,
|
||||
fileName: actualFilename,
|
||||
contentType: actualMimetype,
|
||||
bytes: bytes,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
poolId: poolId,
|
||||
)
|
||||
.then((result) {
|
||||
@@ -272,8 +295,6 @@ class FileUploader {
|
||||
completer.completeError(e);
|
||||
throw e;
|
||||
});
|
||||
|
||||
return completer;
|
||||
}
|
||||
|
||||
/// Gets the MIME type of a UniversalFile.
|
||||
|
||||
@@ -106,20 +106,6 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
|
||||
child: NotificationCard(notification: notification),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(
|
||||
notification.meta['action_uri'],
|
||||
);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissed: () {},
|
||||
dismissType: DismissType.onSwipe,
|
||||
displayDuration: const Duration(seconds: 5),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class NotificationCard extends HookConsumerWidget {
|
||||
final SnNotification notification;
|
||||
@@ -14,9 +17,28 @@ class NotificationCard extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final icon = Symbols.info;
|
||||
|
||||
return Card(
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('solian://')) {
|
||||
uri = uri.replaceFirst('solian://', '');
|
||||
}
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(
|
||||
notification.meta['action_uri'],
|
||||
);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
@@ -67,6 +89,7 @@ class NotificationCard extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
import 'package:island/pods/activity/activity_rpc.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.dart';
|
||||
@@ -15,57 +16,61 @@ import 'package:island/widgets/tour/tour.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||
class AppWrapper extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
const AppWrapper({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
useEffect(() {
|
||||
StreamSubscription? ntySubs;
|
||||
StreamSubscription? appLinksSubs;
|
||||
Future(() async {
|
||||
final appLinks = AppLinks();
|
||||
ConsumerState<AppWrapper> createState() => _AppWrapperState();
|
||||
}
|
||||
|
||||
if (context.mounted) ntySubs = setupNotificationListener(context, ref);
|
||||
class _AppWrapperState extends ConsumerState<AppWrapper>
|
||||
with ProtocolListener, TrayListener {
|
||||
StreamSubscription? ntySubs;
|
||||
bool networkStateShowing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
protocolHandler.addListener(this);
|
||||
Future(() async {
|
||||
if (mounted) ntySubs = setupNotificationListener(context, ref);
|
||||
|
||||
final sharingService = SharingIntentService();
|
||||
if (context.mounted) sharingService.initialize(context);
|
||||
if (context.mounted) UpdateService().checkForUpdates(context);
|
||||
if (mounted) sharingService.initialize(context);
|
||||
if (mounted) UpdateService().checkForUpdates(context);
|
||||
|
||||
TrayService.instance.initialize(this);
|
||||
|
||||
ref.read(rpcServerStateProvider.notifier).start();
|
||||
|
||||
final initialUri = await appLinks.getLatestLink();
|
||||
if (initialUri != null && context.mounted) {
|
||||
final initialUrl = await protocolHandler.getInitialUrl();
|
||||
if (initialUrl != null && mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handleDeepLink(initialUri, ref);
|
||||
_handleDeepLink(Uri.parse(initialUrl), ref);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
appLinksSubs = appLinks.uriLinkStream.listen((uri) {
|
||||
_handleDeepLink(uri, ref);
|
||||
});
|
||||
});
|
||||
|
||||
return () {
|
||||
@override
|
||||
void dispose() {
|
||||
protocolHandler.removeListener(this);
|
||||
ref.read(rpcServerProvider).stop();
|
||||
TrayService.instance.dispose(this);
|
||||
ntySubs?.cancel();
|
||||
appLinksSubs?.cancel();
|
||||
};
|
||||
}, const []);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final wsNotifier = ref.watch(websocketStateProvider.notifier);
|
||||
final websocketState = ref.watch(websocketStateProvider);
|
||||
|
||||
final networkStateShowing = useState(false);
|
||||
|
||||
if (websocketState == WebSocketState.duplicateDevice()) {
|
||||
if (!networkStateShowing.value) {
|
||||
if (!networkStateShowing) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
networkStateShowing.value = true;
|
||||
setState(() => networkStateShowing = true);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -73,12 +78,17 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||
builder:
|
||||
(context) =>
|
||||
NetworkStatusSheet(onReconnect: () => wsNotifier.connect()),
|
||||
).then((_) => networkStateShowing.value = false);
|
||||
).then((_) => setState(() => networkStateShowing = false));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return TourTriggerWidget(key: UniqueKey(), child: child);
|
||||
return TourTriggerWidget(key: UniqueKey(), child: widget.child);
|
||||
}
|
||||
|
||||
@override
|
||||
void onProtocolUrlReceived(String url) {
|
||||
_handleDeepLink(Uri.parse(url), ref);
|
||||
}
|
||||
|
||||
void _trayIconPrimaryAction() {
|
||||
@@ -106,13 +116,17 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||
|
||||
void _handleDeepLink(Uri uri, WidgetRef ref) {
|
||||
final router = ref.read(routerProvider);
|
||||
String path = '/${uri.path}';
|
||||
String path = '/${uri.host}${uri.path}';
|
||||
if (uri.queryParameters.isNotEmpty) {
|
||||
path =
|
||||
Uri.parse(
|
||||
path,
|
||||
).replace(queryParameters: uri.queryParameters).toString();
|
||||
}
|
||||
router.go(path);
|
||||
router.push(path);
|
||||
if (!kIsWeb &&
|
||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
windowManager.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.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/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:island/widgets/content/cloud_file_lightbox.dart';
|
||||
import 'package:island/widgets/content/sensitive.dart';
|
||||
import 'package:material_symbols_icons/symbols.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';
|
||||
|
||||
@@ -47,13 +38,100 @@ class CloudFileList extends HookConsumerWidget {
|
||||
});
|
||||
|
||||
double calculateAspectRatio() {
|
||||
double total = 0;
|
||||
for (var ratio in files.map((e) => e.fileMeta?['ratio'] ?? 1)) {
|
||||
if (ratio is double) total += ratio;
|
||||
if (ratio is String) total += double.parse(ratio);
|
||||
final ratios = <double>[];
|
||||
|
||||
// Collect all valid ratios
|
||||
for (final file in files) {
|
||||
final meta = file.fileMeta;
|
||||
if (meta is Map<String, dynamic> && meta.containsKey('ratio')) {
|
||||
final ratioValue = meta['ratio'];
|
||||
if (ratioValue is num && ratioValue > 0) {
|
||||
ratios.add(ratioValue.toDouble());
|
||||
} else if (ratioValue is String) {
|
||||
try {
|
||||
final parsed = double.parse(ratioValue);
|
||||
if (parsed > 0) ratios.add(parsed);
|
||||
} catch (_) {
|
||||
// Skip invalid string ratios
|
||||
}
|
||||
if (total == 0) return 1;
|
||||
return total / files.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ratios.isEmpty) {
|
||||
// Default to 4:3 aspect ratio when no valid ratios found
|
||||
return 4 / 3;
|
||||
}
|
||||
|
||||
if (ratios.length == 1) {
|
||||
return ratios.first;
|
||||
}
|
||||
|
||||
// Group similar ratios and find the most common one
|
||||
final commonRatios = <double, int>{};
|
||||
|
||||
// Common aspect ratios to round to (with tolerance)
|
||||
const tolerance = 0.05;
|
||||
final standardRatios = [
|
||||
1.0,
|
||||
4 / 3,
|
||||
3 / 2,
|
||||
16 / 9,
|
||||
5 / 3,
|
||||
5 / 4,
|
||||
7 / 5,
|
||||
9 / 16,
|
||||
2 / 3,
|
||||
3 / 4,
|
||||
4 / 5,
|
||||
];
|
||||
|
||||
for (final ratio in ratios) {
|
||||
// Find the closest standard ratio within tolerance
|
||||
double closestRatio = ratio;
|
||||
double minDiff = double.infinity;
|
||||
|
||||
for (final standard in standardRatios) {
|
||||
final diff = (ratio - standard).abs();
|
||||
if (diff < minDiff && diff <= tolerance) {
|
||||
minDiff = diff;
|
||||
closestRatio = standard;
|
||||
}
|
||||
}
|
||||
|
||||
// If no standard ratio is close enough, keep original
|
||||
if (minDiff == double.infinity || minDiff > tolerance) {
|
||||
closestRatio = ratio;
|
||||
}
|
||||
|
||||
commonRatios[closestRatio] = (commonRatios[closestRatio] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Find the most frequent ratio(s)
|
||||
int maxCount = 0;
|
||||
final mostFrequent = <double>[];
|
||||
|
||||
for (final entry in commonRatios.entries) {
|
||||
if (entry.value > maxCount) {
|
||||
maxCount = entry.value;
|
||||
mostFrequent.clear();
|
||||
mostFrequent.add(entry.key);
|
||||
} else if (entry.value == maxCount) {
|
||||
mostFrequent.add(entry.key);
|
||||
}
|
||||
}
|
||||
|
||||
// If only one most frequent ratio, return it
|
||||
if (mostFrequent.length == 1) {
|
||||
return mostFrequent.first;
|
||||
}
|
||||
|
||||
// If multiple ratios have the same highest frequency, use median of them
|
||||
mostFrequent.sort();
|
||||
final mid = mostFrequent.length ~/ 2;
|
||||
return mostFrequent.length.isEven
|
||||
? (mostFrequent[mid - 1] + mostFrequent[mid]) / 2
|
||||
: mostFrequent[mid];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -90,7 +168,7 @@ class CloudFileList extends HookConsumerWidget {
|
||||
}
|
||||
if (!disableZoomIn) {
|
||||
context.pushTransparentRoute(
|
||||
CloudFileZoomIn(item: file, heroTag: heroTags[i]),
|
||||
CloudFileLightbox(item: file, heroTag: heroTags[i]),
|
||||
rootNavigator: true,
|
||||
);
|
||||
}
|
||||
@@ -151,7 +229,7 @@ class CloudFileList extends HookConsumerWidget {
|
||||
}
|
||||
if (!disableZoomIn) {
|
||||
context.pushTransparentRoute(
|
||||
CloudFileZoomIn(item: files.first, heroTag: heroTags.first),
|
||||
CloudFileLightbox(item: files.first, heroTag: heroTags.first),
|
||||
rootNavigator: true,
|
||||
);
|
||||
}
|
||||
@@ -169,10 +247,7 @@ class CloudFileList extends HookConsumerWidget {
|
||||
child:
|
||||
isAudio
|
||||
? widgetItem
|
||||
: AspectRatio(
|
||||
aspectRatio: calculateAspectRatio(),
|
||||
child: widgetItem,
|
||||
),
|
||||
: IntrinsicWidth(child: IntrinsicHeight(child: widgetItem)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,15 +263,20 @@ class CloudFileList extends HookConsumerWidget {
|
||||
aspectRatio: calculateAspectRatio(),
|
||||
child: Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: CarouselView(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth =
|
||||
constraints.maxWidth.isFinite
|
||||
? constraints.maxWidth
|
||||
: MediaQuery.of(context).size.width;
|
||||
final itemExtent = math.min(
|
||||
math.min(availableWidth * 0.75, maxWidth * 0.75).toDouble(),
|
||||
640.0,
|
||||
);
|
||||
|
||||
return CarouselView(
|
||||
itemSnapping: true,
|
||||
itemExtent: math.min(
|
||||
math.min(
|
||||
MediaQuery.of(context).size.width * 0.75,
|
||||
maxWidth * 0.75,
|
||||
),
|
||||
640,
|
||||
),
|
||||
itemExtent: itemExtent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
@@ -231,11 +311,13 @@ class CloudFileList extends HookConsumerWidget {
|
||||
}
|
||||
if (!disableZoomIn) {
|
||||
context.pushTransparentRoute(
|
||||
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
|
||||
CloudFileLightbox(item: files[i], heroTag: heroTags[i]),
|
||||
rootNavigator: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -273,7 +355,7 @@ class CloudFileList extends HookConsumerWidget {
|
||||
}
|
||||
if (!disableZoomIn) {
|
||||
context.pushTransparentRoute(
|
||||
CloudFileZoomIn(
|
||||
CloudFileLightbox(
|
||||
item: files[index],
|
||||
heroTag: heroTags[index],
|
||||
),
|
||||
@@ -305,211 +387,6 @@ class CloudFileList extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class CloudFileZoomIn extends HookConsumerWidget {
|
||||
final SnCloudFile item;
|
||||
final String heroTag;
|
||||
const CloudFileZoomIn({super.key, required this.item, required this.heroTag});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final photoViewController = useMemoized(() => PhotoViewController(), []);
|
||||
final rotation = useState(0);
|
||||
|
||||
final showOriginal = useState(false);
|
||||
|
||||
Future<void> saveToGallery() async {
|
||||
try {
|
||||
// Show loading indicator
|
||||
showSnackBar('Saving image...');
|
||||
|
||||
// Get the image URL
|
||||
final client = ref.watch(apiClientProvider);
|
||||
|
||||
// Create a temporary file to save the image
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
var extName = extension(item.name).trim();
|
||||
if (extName.isEmpty) {
|
||||
extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg';
|
||||
}
|
||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||
|
||||
await client.download(
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
);
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
// Save to gallery
|
||||
await Gal.putImage(filePath, album: 'Solar Network');
|
||||
// Show success message
|
||||
showSnackBar('Image saved to gallery');
|
||||
} else {
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
showSnackBar('Image saved to $filePath');
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
void showInfoSheet() {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
}
|
||||
|
||||
final shadow = [
|
||||
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
||||
];
|
||||
|
||||
return DismissiblePage(
|
||||
isFullScreen: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
onDismissed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PhotoView(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.9),
|
||||
),
|
||||
controller: photoViewController,
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: heroTag),
|
||||
imageProvider: CloudImageWidget.provider(
|
||||
fileId: item.id,
|
||||
serverUrl: serverUrl,
|
||||
original: showOriginal.value,
|
||||
),
|
||||
// Apply rotation transformation
|
||||
customSize: MediaQuery.of(context).size,
|
||||
basePosition: Alignment.center,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
),
|
||||
// Close button and save button
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
right: 16,
|
||||
left: 16,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (!kIsWeb)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.save_alt,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () async {
|
||||
saveToGallery();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showOriginal.value = !showOriginal.value;
|
||||
},
|
||||
icon: Icon(
|
||||
showOriginal.value ? Symbols.hd : Symbols.sd,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: Colors.white, shadows: shadow),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Rotation controls
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.info_outline,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: showInfoSheet,
|
||||
),
|
||||
Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.remove,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () {
|
||||
photoViewController.scale =
|
||||
(photoViewController.scale ?? 1) - 0.05;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.add, color: Colors.white, shadows: shadow),
|
||||
onPressed: () {
|
||||
photoViewController.scale =
|
||||
(photoViewController.scale ?? 1) + 0.05;
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.rotate_left,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
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;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CloudFileListEntry extends HookConsumerWidget {
|
||||
final SnCloudFile file;
|
||||
final String heroTag;
|
||||
@@ -535,15 +412,8 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
||||
final lockedByDS = dataSaving && !showDataSaving.value;
|
||||
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
||||
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
||||
final hasRatio =
|
||||
meta.containsKey('ratio') &&
|
||||
(meta['ratio'] is num && (meta['ratio'] as num) != 0);
|
||||
final ratio =
|
||||
(meta['ratio'] is num && (meta['ratio'] as num) != 0)
|
||||
? (meta['ratio'] as num).toDouble()
|
||||
: 1.0;
|
||||
|
||||
final fit = hasRatio ? BoxFit.cover : BoxFit.contain;
|
||||
final fit = BoxFit.cover;
|
||||
|
||||
Widget bg = const SizedBox.shrink();
|
||||
if (isImage) {
|
||||
@@ -551,7 +421,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
||||
bg = BlurHash(hash: meta['blur'] as String);
|
||||
} else if (!lockedByDS && !lockedByMature) {
|
||||
bg = ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
imageFilter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||
child: CloudFileWidget(
|
||||
fit: BoxFit.cover,
|
||||
item: file,
|
||||
@@ -581,7 +451,9 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
||||
fit: fit,
|
||||
useInternalGate: false,
|
||||
))
|
||||
: AspectRatio(aspectRatio: ratio, child: const SizedBox.shrink());
|
||||
: IntrinsicWidth(
|
||||
child: IntrinsicHeight(child: const SizedBox.shrink()),
|
||||
);
|
||||
|
||||
Widget overlays;
|
||||
if (lockedByDS) {
|
||||
|
||||
231
lib/widgets/content/cloud_file_lightbox.dart
Normal file
231
lib/widgets/content/cloud_file_lightbox.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
import 'cloud_files.dart';
|
||||
|
||||
class CloudFileLightbox extends HookConsumerWidget {
|
||||
final SnCloudFile item;
|
||||
final String heroTag;
|
||||
const CloudFileLightbox({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.heroTag,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final photoViewController = useMemoized(() => PhotoViewController(), []);
|
||||
final rotation = useState(0);
|
||||
|
||||
final showOriginal = useState(false);
|
||||
|
||||
Future<void> saveToGallery() async {
|
||||
try {
|
||||
// Show loading indicator
|
||||
showSnackBar('Saving image...');
|
||||
|
||||
// Get the image URL
|
||||
final client = ref.watch(apiClientProvider);
|
||||
|
||||
// Create a temporary file to save the image
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
var extName = extension(item.name).trim();
|
||||
if (extName.isEmpty) {
|
||||
extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg';
|
||||
}
|
||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||
|
||||
await client.download(
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
);
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
// Save to gallery
|
||||
await Gal.putImage(filePath, album: 'Solar Network');
|
||||
// Show success message
|
||||
showSnackBar('Image saved to gallery');
|
||||
} else {
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
showSnackBar('Image saved to $filePath');
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
void showInfoSheet() {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
}
|
||||
|
||||
final shadow = [
|
||||
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
||||
];
|
||||
|
||||
return DismissiblePage(
|
||||
isFullScreen: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
onDismissed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PhotoView(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.9),
|
||||
),
|
||||
controller: photoViewController,
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: heroTag),
|
||||
imageProvider: CloudImageWidget.provider(
|
||||
fileId: item.id,
|
||||
serverUrl: serverUrl,
|
||||
original: showOriginal.value,
|
||||
),
|
||||
// Apply rotation transformation
|
||||
customSize: MediaQuery.of(context).size,
|
||||
basePosition: Alignment.center,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
),
|
||||
// Close button and save button
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
right: 16,
|
||||
left: 16,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (!kIsWeb)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.save_alt,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () async {
|
||||
saveToGallery();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showOriginal.value = !showOriginal.value;
|
||||
},
|
||||
icon: Icon(
|
||||
showOriginal.value ? Symbols.hd : Symbols.sd,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: Colors.white, shadows: shadow),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Rotation controls
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.info_outline,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: showInfoSheet,
|
||||
),
|
||||
Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.remove,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () {
|
||||
photoViewController.scale =
|
||||
(photoViewController.scale ?? 1) - 0.05;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.add, color: Colors.white, shadows: shadow),
|
||||
onPressed: () {
|
||||
photoViewController.scale =
|
||||
(photoViewController.scale ?? 1) + 0.05;
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.rotate_left,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
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;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -371,7 +371,15 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
var content = switch (item.mimeType?.split('/').firstOrNull) {
|
||||
'image' => AspectRatio(
|
||||
'image' =>
|
||||
ratio == 1.0
|
||||
? IntrinsicHeight(
|
||||
child:
|
||||
(useInternalGate && dataSaving && !unlocked.value)
|
||||
? dataPlaceHolder(Symbols.image)
|
||||
: cloudImage(),
|
||||
)
|
||||
: AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child:
|
||||
(useInternalGate && dataSaving && !unlocked.value)
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/models/embed.dart';
|
||||
import 'package:island/models/poll.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/utils/mapping.dart';
|
||||
import 'package:island/widgets/content/embed/link.dart';
|
||||
@@ -54,13 +53,10 @@ class EmbedListWidget extends StatelessWidget {
|
||||
vertical: 8,
|
||||
),
|
||||
child:
|
||||
embedData['poll'] == null
|
||||
? const Text('Poll was not loaded...')
|
||||
embedData['id'] == null
|
||||
? const Text('Poll was unavailable...')
|
||||
: PollSubmit(
|
||||
initialAnswers:
|
||||
embedData['poll']?['user_answer']?['answer'],
|
||||
stats: embedData['poll']?['stats'],
|
||||
poll: SnPollWithStats.fromJson(embedData['poll']),
|
||||
pollId: embedData['id'],
|
||||
onSubmit: (_) {},
|
||||
isReadonly: !isInteractive,
|
||||
isInitiallyExpanded: isFullPost,
|
||||
|
||||
@@ -10,6 +10,8 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/screens/account/profile.dart';
|
||||
import 'package:island/screens/creators/publishers_form.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/markdown_latex.dart';
|
||||
@@ -397,7 +399,13 @@ class MentionChipSpanNode extends SpanNode {
|
||||
onTap: () => onTap(type, id),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 5,
|
||||
right: 7,
|
||||
top: 2.5,
|
||||
bottom: 2.5,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
@@ -411,18 +419,58 @@ class MentionChipSpanNode extends SpanNode {
|
||||
color: backgroundColor.withOpacity(0.5),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(32)),
|
||||
),
|
||||
child: Icon(
|
||||
switch (parts.first.isEmpty ? 'u' : parts.first) {
|
||||
child: switch (parts.length == 1 ? 'u' : parts.first) {
|
||||
'u' => Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final userData = ref.watch(accountProvider(parts.last));
|
||||
return userData.when(
|
||||
data:
|
||||
(data) => ProfilePictureWidget(
|
||||
file: data.profile.picture,
|
||||
fallbackIcon: Symbols.person_rounded,
|
||||
radius: 9,
|
||||
),
|
||||
error: (_, _) => const Icon(Symbols.close),
|
||||
loading:
|
||||
() => const SizedBox(
|
||||
width: 9,
|
||||
height: 9,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
'p' => Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final pubData = ref.watch(publisherProvider(parts.last));
|
||||
return pubData.when(
|
||||
data:
|
||||
(data) => ProfilePictureWidget(
|
||||
file: data?.picture,
|
||||
fallbackIcon: Symbols.design_services_rounded,
|
||||
radius: 9,
|
||||
),
|
||||
error: (_, _) => const Icon(Symbols.close),
|
||||
loading:
|
||||
() => const SizedBox(
|
||||
width: 9,
|
||||
height: 9,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_ => Icon(
|
||||
(switch (parts.length == 1 ? 'u' : parts.first) {
|
||||
'c' => Symbols.forum_rounded,
|
||||
'r' => Symbols.group_rounded,
|
||||
'u' => Symbols.person_rounded,
|
||||
'p' => Symbols.edit_rounded,
|
||||
_ => Symbols.person_rounded,
|
||||
},
|
||||
}),
|
||||
size: 14,
|
||||
color: foregroundColor,
|
||||
fill: 1,
|
||||
).padding(all: 2),
|
||||
},
|
||||
),
|
||||
Text(
|
||||
parts.last,
|
||||
|
||||
@@ -4,15 +4,15 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/models/poll.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/creators/poll/poll_list.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/poll/poll_stats_widget.dart';
|
||||
|
||||
class PollSubmit extends ConsumerStatefulWidget {
|
||||
const PollSubmit({
|
||||
super.key,
|
||||
required this.poll,
|
||||
required this.pollId,
|
||||
required this.onSubmit,
|
||||
required this.stats,
|
||||
this.initialAnswers,
|
||||
this.onCancel,
|
||||
this.showProgress = true,
|
||||
@@ -20,14 +20,13 @@ class PollSubmit extends ConsumerStatefulWidget {
|
||||
this.isInitiallyExpanded = false,
|
||||
});
|
||||
|
||||
final SnPollWithStats poll;
|
||||
final String pollId;
|
||||
|
||||
/// Callback when user submits all answers. Map questionId -> answer.
|
||||
final void Function(Map<String, dynamic> answers) onSubmit;
|
||||
|
||||
/// Optional initial answers, keyed by questionId.
|
||||
final Map<String, dynamic>? initialAnswers;
|
||||
final Map<String, dynamic>? stats;
|
||||
|
||||
/// Optional cancel callback.
|
||||
final VoidCallback? onCancel;
|
||||
@@ -45,7 +44,7 @@ class PollSubmit extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
late final List<SnPollQuestion> _questions;
|
||||
List<SnPollQuestion>? _questions;
|
||||
int _index = 0;
|
||||
bool _submitting = false;
|
||||
bool _isModifying = false; // New state to track if user is modifying answers
|
||||
@@ -66,14 +65,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Ensure questions are ordered by `order`
|
||||
_questions = [...widget.poll.questions]
|
||||
..sort((a, b) => a.order.compareTo(b.order));
|
||||
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
||||
// Set initial collapse state based on the parameter
|
||||
_isCollapsed = !widget.isInitiallyExpanded;
|
||||
if (!widget.isReadonly) {
|
||||
_loadCurrentIntoLocalState();
|
||||
// If initial answers are provided, set _isModifying to false initially
|
||||
// so the "Modify" button is shown.
|
||||
if (widget.initialAnswers != null && widget.initialAnswers!.isNotEmpty) {
|
||||
@@ -82,25 +77,27 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeFromPollData(SnPollWithStats poll) {
|
||||
// Initialize answers from poll data if available
|
||||
if (poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty) {
|
||||
_answers = Map<String, dynamic>.from(poll.userAnswer!.answer);
|
||||
if (!widget.isReadonly && !_isModifying) {
|
||||
_isModifying = false; // Show modify button if user has answered
|
||||
}
|
||||
}
|
||||
_loadCurrentIntoLocalState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PollSubmit oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.poll.id != widget.poll.id) {
|
||||
if (oldWidget.pollId != widget.pollId) {
|
||||
_index = 0;
|
||||
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
||||
_questions
|
||||
..clear()
|
||||
..addAll(
|
||||
[...widget.poll.questions]
|
||||
..sort((a, b) => a.order.compareTo(b.order)),
|
||||
);
|
||||
if (!widget.isReadonly) {
|
||||
_loadCurrentIntoLocalState();
|
||||
// If poll ID changes, reset modification state
|
||||
// Reset modification state when poll changes
|
||||
_isModifying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -108,7 +105,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
SnPollQuestion get _current => _questions[_index];
|
||||
SnPollQuestion get _current => _questions![_index];
|
||||
|
||||
void _loadCurrentIntoLocalState() {
|
||||
final q = _current;
|
||||
@@ -201,7 +198,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submitToServer() async {
|
||||
Future<void> _submitToServer(SnPollWithStats poll) async {
|
||||
// Persist current question before final submit
|
||||
_persistCurrentAnswer();
|
||||
|
||||
@@ -213,7 +210,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
final dio = ref.read(apiClientProvider);
|
||||
|
||||
await dio.post(
|
||||
'/sphere/polls/${widget.poll.id}/answer',
|
||||
'/sphere/polls/${poll.id}/answer',
|
||||
data: {'answer': _answers},
|
||||
);
|
||||
|
||||
@@ -233,17 +230,17 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
}
|
||||
}
|
||||
|
||||
void _next() {
|
||||
void _next(SnPollWithStats poll) {
|
||||
if (_submitting) return;
|
||||
_persistCurrentAnswer();
|
||||
if (_index < _questions.length - 1) {
|
||||
if (_index < _questions!.length - 1) {
|
||||
setState(() {
|
||||
_index++;
|
||||
_loadCurrentIntoLocalState();
|
||||
});
|
||||
} else {
|
||||
// Final submit to API
|
||||
_submitToServer();
|
||||
_submitToServer(poll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,41 +258,15 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
Widget _buildHeader(BuildContext context, SnPollWithStats poll) {
|
||||
final q = _current;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.poll.title != null || widget.poll.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.poll.title != null)
|
||||
Text(
|
||||
widget.poll.title!,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (widget.poll.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
widget.poll.description!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.showProgress &&
|
||||
_isModifying) // Only show progress when modifying
|
||||
Text(
|
||||
'${_index + 1} / ${_questions.length}',
|
||||
'${_index + 1} / ${_questions!.length}',
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
Row(
|
||||
@@ -334,12 +305,18 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStats(BuildContext context, SnPollQuestion q) {
|
||||
return PollStatsWidget(question: q, stats: widget.stats);
|
||||
Widget _buildStats(
|
||||
BuildContext context,
|
||||
SnPollQuestion q,
|
||||
Map<String, dynamic>? stats,
|
||||
) {
|
||||
return PollStatsWidget(question: q, stats: stats);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
|
||||
Widget _buildBody(BuildContext context, SnPollWithStats poll) {
|
||||
final hasUserAnswer =
|
||||
poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty;
|
||||
if (hasUserAnswer && !widget.isReadonly && !_isModifying) {
|
||||
return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying
|
||||
}
|
||||
final q = _current;
|
||||
@@ -449,11 +426,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavBar(BuildContext context) {
|
||||
final isLast = _index == _questions.length - 1;
|
||||
Widget _buildNavBar(BuildContext context, SnPollWithStats poll) {
|
||||
final isLast = _index == _questions!.length - 1;
|
||||
final canProceed = _isCurrentAnswered() && !_submitting;
|
||||
final hasUserAnswer =
|
||||
poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty;
|
||||
|
||||
if (widget.initialAnswers != null && !_isModifying && !widget.isReadonly) {
|
||||
if (hasUserAnswer && !_isModifying && !widget.isReadonly) {
|
||||
// If poll is submitted and not in modification mode, show "Modify" button
|
||||
return FilledButton.icon(
|
||||
icon: const Icon(Icons.edit),
|
||||
@@ -498,32 +477,32 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
)
|
||||
: Icon(isLast ? Icons.check : Icons.arrow_forward),
|
||||
label: Text(isLast ? 'submit'.tr() : 'next'.tr()),
|
||||
onPressed: canProceed ? _next : null,
|
||||
onPressed: canProceed ? () => _next(poll) : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubmittedView(BuildContext context) {
|
||||
Widget _buildSubmittedView(BuildContext context, SnPollWithStats poll) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.poll.title != null || widget.poll.description != null)
|
||||
if (poll.title != null || poll.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.poll.title?.isNotEmpty ?? false)
|
||||
if (poll.title?.isNotEmpty ?? false)
|
||||
Text(
|
||||
widget.poll.title!,
|
||||
poll.title!,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (widget.poll.description?.isNotEmpty ?? false)
|
||||
if (poll.description?.isNotEmpty ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
widget.poll.description!,
|
||||
poll.description!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
@@ -534,7 +513,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
],
|
||||
),
|
||||
),
|
||||
for (final q in _questions)
|
||||
for (final q in _questions!)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
@@ -574,7 +553,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildStats(context, q),
|
||||
_buildStats(context, q, poll.stats),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -582,26 +561,26 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReadonlyView(BuildContext context) {
|
||||
Widget _buildReadonlyView(BuildContext context, SnPollWithStats poll) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.poll.title != null || widget.poll.description != null)
|
||||
if (poll.title != null || poll.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.poll.title != null)
|
||||
if (poll.title != null)
|
||||
Text(
|
||||
widget.poll.title!,
|
||||
poll.title!,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (widget.poll.description != null)
|
||||
if (poll.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
widget.poll.description!,
|
||||
poll.description!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
@@ -612,7 +591,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
],
|
||||
),
|
||||
),
|
||||
for (final q in _questions)
|
||||
for (final q in _questions!)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
@@ -652,7 +631,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildStats(context, q),
|
||||
_buildStats(context, q, poll.stats),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -660,7 +639,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCollapsedView(BuildContext context) {
|
||||
Widget _buildCollapsedView(BuildContext context, SnPollWithStats poll) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -670,20 +649,20 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.poll.title != null)
|
||||
if (poll.title != null)
|
||||
Text(
|
||||
widget.poll.title!,
|
||||
poll.title!,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (widget.poll.description != null)
|
||||
if (poll.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
widget.poll.description!,
|
||||
poll.description!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
@@ -697,7 +676,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
'${_questions.length} question${_questions.length == 1 ? '' : 's'}',
|
||||
'${_questions!.length} question${_questions!.length == 1 ? '' : 's'}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
@@ -729,21 +708,48 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_questions.isEmpty) {
|
||||
final pollAsync = ref.watch(pollWithStatsProvider(widget.pollId));
|
||||
|
||||
return pollAsync.when(
|
||||
loading:
|
||||
() => const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
error:
|
||||
(error, stack) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text('Failed to load poll: $error'),
|
||||
),
|
||||
),
|
||||
data: (poll) {
|
||||
// Initialize questions when data is available
|
||||
_questions = [...poll.questions]
|
||||
..sort((a, b) => a.order.compareTo(b.order));
|
||||
|
||||
// Initialize answers from poll data
|
||||
_initializeFromPollData(poll);
|
||||
|
||||
if (_questions!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// If collapsed, show collapsed view for all states
|
||||
if (_isCollapsed) {
|
||||
return _buildCollapsedView(context);
|
||||
return _buildCollapsedView(context, poll);
|
||||
}
|
||||
|
||||
// If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view
|
||||
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
|
||||
final hasUserAnswer =
|
||||
poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty;
|
||||
if (hasUserAnswer && !widget.isReadonly && !_isModifying) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildCollapsedView(context),
|
||||
_buildCollapsedView(context, poll),
|
||||
const SizedBox(height: 8),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -751,8 +757,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
final offset = Tween<Offset>(
|
||||
begin: const Offset(0, -0.1),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
||||
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
||||
).animate(
|
||||
CurvedAnimation(parent: anim, curve: Curves.easeOut),
|
||||
);
|
||||
final fade = CurvedAnimation(
|
||||
parent: anim,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
return FadeTransition(
|
||||
opacity: fade,
|
||||
child: SlideTransition(position: offset, child: child),
|
||||
@@ -761,7 +772,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
child: Column(
|
||||
key: const ValueKey('submitted_expanded'),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [_buildSubmittedView(context), _buildNavBar(context)],
|
||||
children: [
|
||||
_buildSubmittedView(context, poll),
|
||||
_buildNavBar(context, poll),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -773,7 +787,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildCollapsedView(context),
|
||||
_buildCollapsedView(context, poll),
|
||||
const SizedBox(height: 8),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -781,14 +795,19 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
final offset = Tween<Offset>(
|
||||
begin: const Offset(0, -0.1),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
||||
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
||||
).animate(
|
||||
CurvedAnimation(parent: anim, curve: Curves.easeOut),
|
||||
);
|
||||
final fade = CurvedAnimation(
|
||||
parent: anim,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
return FadeTransition(
|
||||
opacity: fade,
|
||||
child: SlideTransition(position: offset, child: child),
|
||||
);
|
||||
},
|
||||
child: _buildReadonlyView(context),
|
||||
child: _buildReadonlyView(context, poll),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -797,7 +816,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildCollapsedView(context),
|
||||
_buildCollapsedView(context, poll),
|
||||
const SizedBox(height: 8),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -806,7 +825,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
begin: const Offset(0, -0.1),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
||||
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
||||
final fade = CurvedAnimation(
|
||||
parent: anim,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
return FadeTransition(
|
||||
opacity: fade,
|
||||
child: SlideTransition(position: offset, child: child),
|
||||
@@ -816,25 +838,27 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
key: const ValueKey('normal_expanded'),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
_buildHeader(context, poll),
|
||||
const SizedBox(height: 12),
|
||||
_AnimatedStep(
|
||||
key: ValueKey(_current.id),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildBody(context),
|
||||
_buildStats(context, _current),
|
||||
_buildBody(context, poll),
|
||||
_buildStats(context, _current, poll.stats),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildNavBar(context),
|
||||
_buildNavBar(context, poll),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -423,6 +423,7 @@ class PostComposeCard extends HookConsumerWidget {
|
||||
state: composeState,
|
||||
originalPost: originalPost,
|
||||
isCompact: true,
|
||||
useSafeArea: isContained,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -14,12 +14,14 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
final ComposeState state;
|
||||
final SnPost? originalPost;
|
||||
final bool isCompact;
|
||||
final bool useSafeArea;
|
||||
|
||||
const ComposeToolbar({
|
||||
super.key,
|
||||
required this.state,
|
||||
this.originalPost,
|
||||
this.isCompact = false,
|
||||
this.useSafeArea = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -200,7 +202,12 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4),
|
||||
).padding(
|
||||
horizontal: 8,
|
||||
top: 4,
|
||||
bottom:
|
||||
useSafeArea ? MediaQuery.of(context).padding.bottom + 4 : 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,6 +24,12 @@ class PostListNotifier extends _$PostListNotifier
|
||||
bool? pinned,
|
||||
bool shuffle = false,
|
||||
bool? includeReplies,
|
||||
bool? mediaOnly,
|
||||
String? queryTerm,
|
||||
String? order,
|
||||
int? periodStart,
|
||||
int? periodEnd,
|
||||
bool orderDesc = true,
|
||||
}) {
|
||||
return fetch(cursor: null);
|
||||
}
|
||||
@@ -36,14 +42,20 @@ class PostListNotifier extends _$PostListNotifier
|
||||
final queryParams = {
|
||||
'offset': offset,
|
||||
'take': _pageSize,
|
||||
'replies': includeReplies,
|
||||
'orderDesc': orderDesc,
|
||||
if (shuffle) 'shuffle': shuffle,
|
||||
if (pubName != null) 'pub': pubName,
|
||||
if (realm != null) 'realm': realm,
|
||||
if (type != null) 'type': type,
|
||||
if (tags != null) 'tags': tags,
|
||||
if (categories != null) 'categories': categories,
|
||||
if (shuffle) 'shuffle': true,
|
||||
if (pinned != null) 'pinned': pinned,
|
||||
if (includeReplies != null) 'includeReplies': includeReplies,
|
||||
if (order != null) 'order': order,
|
||||
if (periodStart != null) 'periodStart': periodStart,
|
||||
if (periodEnd != null) 'periodEnd': periodEnd,
|
||||
if (queryTerm != null) 'query': queryTerm,
|
||||
if (mediaOnly != null) 'media': mediaOnly,
|
||||
};
|
||||
|
||||
final response = await client.get(
|
||||
@@ -82,6 +94,14 @@ class SliverPostList extends HookConsumerWidget {
|
||||
final List<String>? tags;
|
||||
final bool shuffle;
|
||||
final bool? pinned;
|
||||
final bool? includeReplies;
|
||||
final bool? mediaOnly;
|
||||
final String? queryTerm;
|
||||
// Can be "populaurity", other value will be treated as "date"
|
||||
final String? order;
|
||||
final int? periodStart;
|
||||
final int? periodEnd;
|
||||
final bool? orderDesc;
|
||||
final PostItemType itemType;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets? padding;
|
||||
@@ -99,6 +119,13 @@ class SliverPostList extends HookConsumerWidget {
|
||||
this.tags,
|
||||
this.shuffle = false,
|
||||
this.pinned,
|
||||
this.includeReplies,
|
||||
this.mediaOnly,
|
||||
this.queryTerm,
|
||||
this.order,
|
||||
this.orderDesc = true,
|
||||
this.periodStart,
|
||||
this.periodEnd,
|
||||
this.itemType = PostItemType.regular,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
@@ -118,6 +145,13 @@ class SliverPostList extends HookConsumerWidget {
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
pinned: pinned,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
orderDesc: orderDesc ?? true,
|
||||
);
|
||||
return PagingHelperSliverView(
|
||||
provider: provider,
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'post_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$postListNotifierHash() => r'fc139ad4df0deb67bcbb949560319f2f7fbfb503';
|
||||
String _$postListNotifierHash() => r'8241120dc3c2004387c6cf881e5cb9224cbd3a97';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -39,6 +39,12 @@ abstract class _$PostListNotifier
|
||||
late final bool? pinned;
|
||||
late final bool shuffle;
|
||||
late final bool? includeReplies;
|
||||
late final bool? mediaOnly;
|
||||
late final String? queryTerm;
|
||||
late final String? order;
|
||||
late final int? periodStart;
|
||||
late final int? periodEnd;
|
||||
late final bool orderDesc;
|
||||
|
||||
FutureOr<CursorPagingData<SnPost>> build({
|
||||
String? pubName,
|
||||
@@ -49,6 +55,12 @@ abstract class _$PostListNotifier
|
||||
bool? pinned,
|
||||
bool shuffle = false,
|
||||
bool? includeReplies,
|
||||
bool? mediaOnly,
|
||||
String? queryTerm,
|
||||
String? order,
|
||||
int? periodStart,
|
||||
int? periodEnd,
|
||||
bool orderDesc = true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,6 +84,12 @@ class PostListNotifierFamily
|
||||
bool? pinned,
|
||||
bool shuffle = false,
|
||||
bool? includeReplies,
|
||||
bool? mediaOnly,
|
||||
String? queryTerm,
|
||||
String? order,
|
||||
int? periodStart,
|
||||
int? periodEnd,
|
||||
bool orderDesc = true,
|
||||
}) {
|
||||
return PostListNotifierProvider(
|
||||
pubName: pubName,
|
||||
@@ -82,6 +100,12 @@ class PostListNotifierFamily
|
||||
pinned: pinned,
|
||||
shuffle: shuffle,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
orderDesc: orderDesc,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,6 +122,12 @@ class PostListNotifierFamily
|
||||
pinned: provider.pinned,
|
||||
shuffle: provider.shuffle,
|
||||
includeReplies: provider.includeReplies,
|
||||
mediaOnly: provider.mediaOnly,
|
||||
queryTerm: provider.queryTerm,
|
||||
order: provider.order,
|
||||
periodStart: provider.periodStart,
|
||||
periodEnd: provider.periodEnd,
|
||||
orderDesc: provider.orderDesc,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,6 +163,12 @@ class PostListNotifierProvider
|
||||
bool? pinned,
|
||||
bool shuffle = false,
|
||||
bool? includeReplies,
|
||||
bool? mediaOnly,
|
||||
String? queryTerm,
|
||||
String? order,
|
||||
int? periodStart,
|
||||
int? periodEnd,
|
||||
bool orderDesc = true,
|
||||
}) : this._internal(
|
||||
() =>
|
||||
PostListNotifier()
|
||||
@@ -143,7 +179,13 @@ class PostListNotifierProvider
|
||||
..tags = tags
|
||||
..pinned = pinned
|
||||
..shuffle = shuffle
|
||||
..includeReplies = includeReplies,
|
||||
..includeReplies = includeReplies
|
||||
..mediaOnly = mediaOnly
|
||||
..queryTerm = queryTerm
|
||||
..order = order
|
||||
..periodStart = periodStart
|
||||
..periodEnd = periodEnd
|
||||
..orderDesc = orderDesc,
|
||||
from: postListNotifierProvider,
|
||||
name: r'postListNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
@@ -161,6 +203,12 @@ class PostListNotifierProvider
|
||||
pinned: pinned,
|
||||
shuffle: shuffle,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
orderDesc: orderDesc,
|
||||
);
|
||||
|
||||
PostListNotifierProvider._internal(
|
||||
@@ -178,6 +226,12 @@ class PostListNotifierProvider
|
||||
required this.pinned,
|
||||
required this.shuffle,
|
||||
required this.includeReplies,
|
||||
required this.mediaOnly,
|
||||
required this.queryTerm,
|
||||
required this.order,
|
||||
required this.periodStart,
|
||||
required this.periodEnd,
|
||||
required this.orderDesc,
|
||||
}) : super.internal();
|
||||
|
||||
final String? pubName;
|
||||
@@ -188,6 +242,12 @@ class PostListNotifierProvider
|
||||
final bool? pinned;
|
||||
final bool shuffle;
|
||||
final bool? includeReplies;
|
||||
final bool? mediaOnly;
|
||||
final String? queryTerm;
|
||||
final String? order;
|
||||
final int? periodStart;
|
||||
final int? periodEnd;
|
||||
final bool orderDesc;
|
||||
|
||||
@override
|
||||
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
||||
@@ -202,6 +262,12 @@ class PostListNotifierProvider
|
||||
pinned: pinned,
|
||||
shuffle: shuffle,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
orderDesc: orderDesc,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,7 +285,13 @@ class PostListNotifierProvider
|
||||
..tags = tags
|
||||
..pinned = pinned
|
||||
..shuffle = shuffle
|
||||
..includeReplies = includeReplies,
|
||||
..includeReplies = includeReplies
|
||||
..mediaOnly = mediaOnly
|
||||
..queryTerm = queryTerm
|
||||
..order = order
|
||||
..periodStart = periodStart
|
||||
..periodEnd = periodEnd
|
||||
..orderDesc = orderDesc,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
@@ -233,6 +305,12 @@ class PostListNotifierProvider
|
||||
pinned: pinned,
|
||||
shuffle: shuffle,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
orderDesc: orderDesc,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -256,7 +334,13 @@ class PostListNotifierProvider
|
||||
other.tags == tags &&
|
||||
other.pinned == pinned &&
|
||||
other.shuffle == shuffle &&
|
||||
other.includeReplies == includeReplies;
|
||||
other.includeReplies == includeReplies &&
|
||||
other.mediaOnly == mediaOnly &&
|
||||
other.queryTerm == queryTerm &&
|
||||
other.order == order &&
|
||||
other.periodStart == periodStart &&
|
||||
other.periodEnd == periodEnd &&
|
||||
other.orderDesc == orderDesc;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -270,6 +354,12 @@ class PostListNotifierProvider
|
||||
hash = _SystemHash.combine(hash, pinned.hashCode);
|
||||
hash = _SystemHash.combine(hash, shuffle.hashCode);
|
||||
hash = _SystemHash.combine(hash, includeReplies.hashCode);
|
||||
hash = _SystemHash.combine(hash, mediaOnly.hashCode);
|
||||
hash = _SystemHash.combine(hash, queryTerm.hashCode);
|
||||
hash = _SystemHash.combine(hash, order.hashCode);
|
||||
hash = _SystemHash.combine(hash, periodStart.hashCode);
|
||||
hash = _SystemHash.combine(hash, periodEnd.hashCode);
|
||||
hash = _SystemHash.combine(hash, orderDesc.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
@@ -302,6 +392,24 @@ mixin PostListNotifierRef
|
||||
|
||||
/// The parameter `includeReplies` of this provider.
|
||||
bool? get includeReplies;
|
||||
|
||||
/// The parameter `mediaOnly` of this provider.
|
||||
bool? get mediaOnly;
|
||||
|
||||
/// The parameter `queryTerm` of this provider.
|
||||
String? get queryTerm;
|
||||
|
||||
/// The parameter `order` of this provider.
|
||||
String? get order;
|
||||
|
||||
/// The parameter `periodStart` of this provider.
|
||||
int? get periodStart;
|
||||
|
||||
/// The parameter `periodEnd` of this provider.
|
||||
int? get periodEnd;
|
||||
|
||||
/// The parameter `orderDesc` of this provider.
|
||||
bool get orderDesc;
|
||||
}
|
||||
|
||||
class _PostListNotifierProviderElement
|
||||
@@ -331,6 +439,18 @@ class _PostListNotifierProviderElement
|
||||
@override
|
||||
bool? get includeReplies =>
|
||||
(origin as PostListNotifierProvider).includeReplies;
|
||||
@override
|
||||
bool? get mediaOnly => (origin as PostListNotifierProvider).mediaOnly;
|
||||
@override
|
||||
String? get queryTerm => (origin as PostListNotifierProvider).queryTerm;
|
||||
@override
|
||||
String? get order => (origin as PostListNotifierProvider).order;
|
||||
@override
|
||||
int? get periodStart => (origin as PostListNotifierProvider).periodStart;
|
||||
@override
|
||||
int? get periodEnd => (origin as PostListNotifierProvider).periodEnd;
|
||||
@override
|
||||
bool get orderDesc => (origin as PostListNotifierProvider).orderDesc;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
@@ -542,6 +542,7 @@ class ReactionDetailsPopup extends HookConsumerWidget {
|
||||
notifierRefreshable: provider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include <flutter_timezone/flutter_timezone_plugin.h>
|
||||
#include <flutter_udid/flutter_udid_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <irondash_engine_context/irondash_engine_context_plugin.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
@@ -51,9 +50,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
|
||||
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
|
||||
|
||||
@@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_timezone
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
gtk
|
||||
irondash_engine_context
|
||||
livekit_client
|
||||
media_kit_libs_linux
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import app_links
|
||||
import connectivity_plus
|
||||
import device_info_plus
|
||||
import file_picker
|
||||
@@ -31,6 +30,7 @@ import media_kit_video
|
||||
import package_info_plus
|
||||
import pasteboard
|
||||
import path_provider_foundation
|
||||
import protocol_handler_macos
|
||||
import record_macos
|
||||
import screen_retriever_macos
|
||||
import share_plus
|
||||
@@ -47,7 +47,6 @@ import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
@@ -73,6 +72,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"))
|
||||
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
PODS:
|
||||
- app_links (6.4.1):
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
@@ -199,6 +197,8 @@ PODS:
|
||||
- PromisesObjC (2.4.0)
|
||||
- PromisesSwift (2.4.0):
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- protocol_handler_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- record_macos (1.1.0):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
@@ -256,7 +256,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
@@ -284,6 +283,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`)
|
||||
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
|
||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
@@ -323,8 +323,6 @@ SPEC REPOS:
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
||||
connectivity_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||
croppy:
|
||||
@@ -379,6 +377,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
protocol_handler_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
|
||||
record_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||
screen_retriever_macos:
|
||||
@@ -409,7 +409,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
||||
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
||||
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
@@ -454,6 +453,7 @@ SPEC CHECKSUMS:
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_macos: f9cd7b13bcaf6b0425f7410cbe52376cb843a936
|
||||
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
@@ -33,13 +33,26 @@
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticTermination</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict/>
|
||||
<dict />
|
||||
</dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
204
pubspec.lock
204
pubspec.lock
@@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: f871a7d1b686bea1f13722aa51ab31554d05c81f47054d6de48cc8c45153508b
|
||||
sha256: "8a1f5f3020ef2a74fb93f7ab3ef127a8feea33a7a2276279113660784ee7516a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.63"
|
||||
version: "1.3.64"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -49,38 +49,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
app_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_linux
|
||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
app_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_platform_interface
|
||||
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
app_links_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_web
|
||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
archive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -341,10 +309,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -429,10 +397,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
||||
sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.5.0"
|
||||
version: "11.3.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -637,34 +605,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_analytics
|
||||
sha256: "3cfc4089e61e810ffb531af63cfde2c8cfd36f12dc14fdba359e623992311015"
|
||||
sha256: bfb80d92eee10a6585ebd5a7e60de5caf0f2c06329e5676c0578130aea1bfe85
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.3"
|
||||
version: "12.0.4"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
sha256: "775fc18d9b00a014362510a33f76f1f34deb30f69a64edcb41a7dfd0ebd9cf98"
|
||||
sha256: "3b803077907def997044774f6c022d8e9204e9c0f5e205e3572d887c93dafd72"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
version: "5.0.4"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
sha256: "6eafa8fef5fdca6c922ac3e353c9a093c12344a3ba996e65fd40f8db0a00d26f"
|
||||
sha256: "0dbd96dbe77b51185319000c0078477fdcffb4abb0018c362dd9afb9845c1e06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0+3"
|
||||
version: "0.6.1"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "132e1c311bc41e7d387b575df0aacdf24efbf4930365eb61042be5bde3978f03"
|
||||
sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "4.2.1"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -677,50 +645,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: ecde2def458292404a4fcd3731ee4992fd631a0ec359d2d67c33baa8da5ec8ae
|
||||
sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.3.0"
|
||||
firebase_crashlytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: "2f53d0d3c0875105b166f09bdf026026bb74f26930c6ffcd5d65b311ca5a9f58"
|
||||
sha256: c3ebe3ed9f3b1d36c0864a4a28b041fcc2686f11fb2a4f7891319ea8d1d161cc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
version: "5.0.4"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
sha256: de5c857525fc9576cd3fc30fc72422bc2371179ecae110246c0135ae896c6de3
|
||||
sha256: a8ca502fe3aa48b4f0b9e6e3bc0019085a247b5d1214cd342a189457975662db
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.14"
|
||||
version: "3.8.15"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "5021279acd1cb5ccaceaa388e616e82cc4a2e4d862f02637df0e8ab766e6900a"
|
||||
sha256: "22086f857d2340f5d973776cfd542d3fb30cf98e1c643c3aa4a7520bb12745bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.0.3"
|
||||
version: "16.0.4"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: f3a16c51f02055ace2a7c16ccb341c1f1b36b67c13270a48bcef68c1d970bbe8
|
||||
sha256: a59920cbf2eb7c83d34a5f354331210ffec116b216dc72d864d8b8eb983ca398
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.3"
|
||||
version: "4.7.4"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "3eb9a1382caeb95b370f21e36d4a460496af777c9c2ef5df9b90d4803982c069"
|
||||
sha256: "1183e40e6fd2a279a628951cc3b639fcf5ffe7589902632db645011eb70ebefb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
version: "4.1.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -778,10 +746,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_card_swiper
|
||||
sha256: "9fbe75c913c0a01f34f9f98068ad198e396695fcf8abfa433cc53652fceb5617"
|
||||
sha256: "895c6974729b51cf73a35f1b58ab57a0af3293131319e2cbccac3bc57ffcd69f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.1.0"
|
||||
version: "7.2.0"
|
||||
flutter_colorpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1103,10 +1071,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
|
||||
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1201,10 +1169,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: get_it
|
||||
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
|
||||
sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.0"
|
||||
version: "8.3.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1245,14 +1213,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.4"
|
||||
gtk:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gtk
|
||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
highlight:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1957,6 +1917,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
protocol_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: protocol_handler
|
||||
sha256: dc2e2dcb1e0e313c3f43827ec3fa6d98adee6e17edc0c3923ac67efee87479a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_android
|
||||
sha256: "82eb860ca42149e400328f54b85140329a1766d982e94705b68271f6ca73895c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_ios
|
||||
sha256: "0d3a56b8c1926002cb1e32b46b56874759f4dcc8183d389b670864ac041b6ec2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_macos
|
||||
sha256: "6eb8687a84e7da3afbc5660ce046f29d7ecf7976db45a9dadeae6c87147dd710"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_platform_interface
|
||||
sha256: "53776b10526fdc25efdf1abcf68baf57fdfdb75342f4101051db521c9e3f3e5b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_windows
|
||||
sha256: d8f3a58938386aca2c76292757392f4d059d09f11439d6d896d876ebe997f2c4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2395,14 +2403,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2551,18 +2551,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: a24e9ec04e03c2c14b7b41b1afe60e455adef09b244ab4c425ce6c5b8f58c9ce
|
||||
sha256: "825670efc828e18e14ff310cbcf6de91c8ff73b55c75ae6868a2b3cd87e88b6c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_flutter_pdf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_pdf
|
||||
sha256: "8d98edae5c5d3aba2125de49bd37882da124409021d4f3de5730eb93d8247a81"
|
||||
sha256: "50fc39ba628167949e89374488de67cc788646d6c0dce2a9fd047dbeecb841c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2575,50 +2575,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_signaturepad
|
||||
sha256: d2f87273133283efd550370403462739329ad0ad1bdae6a73998be1fb30e9ee1
|
||||
sha256: "6e60af61cec5ee7436b01ecb3fd944602aed42887789a67a27314678ad04d38a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_linux
|
||||
sha256: "1edc9c3408526ad25c7a0d67b0f12a3e427225fd7e87d67319cd6e19bbfaeb45"
|
||||
sha256: fea25c996ed8850504c80c8fe7541aa3dce3d5159af0e92519d13e10a9509601
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_macos
|
||||
sha256: "962911d8cba4d3f5f0bf5dee5ef87cc0b31651431adfad56a51c47057859fb50"
|
||||
sha256: "389326ef84ad9d14858d4f5f14da36267faa894134c38080ae30d55d2e3f4ce9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_platform_interface
|
||||
sha256: a701825a971f1bb8540ad39611872ebc08ed0955a0a9600f263cb6cb85826ce2
|
||||
sha256: "2c3098dc644965feee66f4bf726ef433a51eecc16ccea71e052ba19897f3c2c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_web
|
||||
sha256: e3eda11636a013a7ebab01a573b079d3a52c695474ac7c5239f65d5952d8da82
|
||||
sha256: db5b91493aefb2e9faeb6425ea4f3c5f8eb7907a29ffca2e33564987a9c1c1f4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_windows
|
||||
sha256: "9f8def51da7277bda5796ba27fff357a697689e226be397d7c52e353824cf961"
|
||||
sha256: d2e4d64e5cd96ea678b1ff66588897ce59e17e2685c1153995af53d91327a143
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2719,10 +2719,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: "537e539f48cd82d8ee2240d4330158c7b44c7e043e8e18b5811f2f8f6b7df25a"
|
||||
sha256: c5fd83b0ae4d80be6eaedfad87aaefab8787b333b8ebd064b0e442a81006035b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "0.5.2"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2840,10 +2840,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
version: "4.5.2"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2984,10 +2984,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "1.1.5"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
24
pubspec.yaml
24
pubspec.yaml
@@ -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.3.0+143
|
||||
version: 3.3.0+145
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
@@ -50,7 +50,7 @@ dependencies:
|
||||
flutter_markdown_latex: ^0.3.4
|
||||
markdown: ^7.3.0
|
||||
flutter_highlight: ^0.7.0
|
||||
uuid: ^4.5.1
|
||||
uuid: ^4.5.2
|
||||
url_launcher: ^6.3.2
|
||||
google_fonts: ^6.3.2
|
||||
gap: ^3.0.1
|
||||
@@ -67,7 +67,8 @@ dependencies:
|
||||
flutter_inappwebview: ^6.1.5
|
||||
animations: ^2.1.0
|
||||
package_info_plus: ^9.0.0
|
||||
device_info_plus: ^11.5.0
|
||||
device_info_plus: ^11.3.0
|
||||
protocol_handler: ^0.2.0
|
||||
tus_client_dart:
|
||||
git: https://github.com/LittleSheep2Code/tus_client.git
|
||||
cross_file: ^0.3.5
|
||||
@@ -78,9 +79,9 @@ dependencies:
|
||||
image_picker_android: ^0.8.13+7
|
||||
super_context_menu: ^0.9.1
|
||||
modal_bottom_sheet: ^3.0.0
|
||||
firebase_messaging: ^16.0.3
|
||||
firebase_messaging: ^16.0.4
|
||||
flutter_udid: ^4.0.0
|
||||
firebase_core: ^4.2.0
|
||||
firebase_core: ^4.2.1
|
||||
web_socket_channel: ^3.0.3
|
||||
material_symbols_icons: ^4.2874.0
|
||||
drift: ^2.28.2
|
||||
@@ -93,7 +94,7 @@ dependencies:
|
||||
relative_time: ^5.0.0
|
||||
dropdown_button2: ^2.3.9
|
||||
riverpod_paging_utils: ^0.8.1
|
||||
crypto: ^3.0.6
|
||||
crypto: ^3.0.7
|
||||
avatar_stack: ^3.0.0
|
||||
markdown_widget: ^2.3.2+8
|
||||
visibility_detector: ^0.4.0+2
|
||||
@@ -115,7 +116,7 @@ dependencies:
|
||||
flutter_timezone: ^5.0.1
|
||||
fl_chart: ^1.1.1
|
||||
sign_in_with_apple: ^7.0.1
|
||||
flutter_svg: ^2.2.1
|
||||
flutter_svg: ^2.2.2
|
||||
native_exif: ^0.6.2
|
||||
local_auth: ^3.0.0
|
||||
flutter_secure_storage: ^9.2.4
|
||||
@@ -134,13 +135,13 @@ dependencies:
|
||||
flutter_app_update: ^3.2.2
|
||||
archive: ^4.0.7
|
||||
process_run: ^1.2.4
|
||||
firebase_crashlytics: ^5.0.3
|
||||
firebase_analytics: ^12.0.3
|
||||
firebase_crashlytics: ^5.0.4
|
||||
firebase_analytics: ^12.0.4
|
||||
material_color_utilities: ^0.11.1
|
||||
screenshot: ^3.0.0
|
||||
flutter_card_swiper: ^7.1.0
|
||||
flutter_card_swiper: ^7.2.0
|
||||
file_saver: ^0.3.1
|
||||
tray_manager: ^0.5.1
|
||||
tray_manager: ^0.5.2
|
||||
flutter_webrtc: ^1.2.0
|
||||
flutter_local_notifications: ^19.5.0
|
||||
wakelock_plus: ^1.4.0
|
||||
@@ -158,7 +159,6 @@ dependencies:
|
||||
talker_logger: ^5.0.2
|
||||
talker_dio_logger: ^5.0.2
|
||||
talker_riverpod_logger: ^5.0.1
|
||||
app_links: ^6.4.1
|
||||
syncfusion_flutter_pdfviewer: ^31.1.21
|
||||
swipe_to: ^1.0.6
|
||||
fl_heatmap: ^0.4.6
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
; ==================================================
|
||||
#define AppVersion "3.2.0"
|
||||
#define BuildNumber "134"
|
||||
#define AppVersion "3.3.0"
|
||||
#define BuildNumber "144"
|
||||
; ==================================================
|
||||
|
||||
#define FullVersion AppVersion + "." + BuildNumber
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <dart_ipc/dart_ipc_plugin_c_api.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
@@ -25,6 +24,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 <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
@@ -38,8 +38,6 @@
|
||||
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DartIpcPluginCApiRegisterWithRegistrar(
|
||||
@@ -76,6 +74,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||
PasteboardPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
connectivity_plus
|
||||
dart_ipc
|
||||
file_saver
|
||||
@@ -22,6 +21,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
pasteboard
|
||||
protocol_handler_windows
|
||||
record_windows
|
||||
screen_retriever_windows
|
||||
share_plus
|
||||
|
||||
@@ -1,51 +1,23 @@
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
#include "app_links/app_links_plugin_c_api.h"
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
bool SendAppLinkToInstance(const std::wstring& title) {
|
||||
// Find our exact window
|
||||
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str());
|
||||
|
||||
if (hwnd) {
|
||||
// Dispatch new link to current window
|
||||
SendAppLink(hwnd);
|
||||
|
||||
// (Optional) Restore our window to front in same state
|
||||
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
|
||||
GetWindowPlacement(hwnd, &place);
|
||||
|
||||
switch(place.showCmd) {
|
||||
case SW_SHOWMAXIMIZED:
|
||||
ShowWindow(hwnd, SW_SHOWMAXIMIZED);
|
||||
break;
|
||||
case SW_SHOWMINIMIZED:
|
||||
ShowWindow(hwnd, SW_RESTORE);
|
||||
break;
|
||||
default:
|
||||
ShowWindow(hwnd, SW_NORMAL);
|
||||
break;
|
||||
}
|
||||
|
||||
SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
|
||||
SetForegroundWindow(hwnd);
|
||||
// END (Optional) Restore
|
||||
|
||||
// Window has been found, don't create another one.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command)
|
||||
{
|
||||
if (SendAppLinkToInstance(L"solian")) {
|
||||
return EXIT_SUCCESS;
|
||||
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"Solian");
|
||||
if (hwnd != NULL)
|
||||
{
|
||||
DispatchToProtocolHandler(hwnd);
|
||||
|
||||
::ShowWindow(hwnd, SW_NORMAL);
|
||||
::SetForegroundWindow(hwnd);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
|
||||
Reference in New Issue
Block a user