Compare commits
20 Commits
f542d9fa97
...
3.3.0+144
| Author | SHA1 | Date | |
|---|---|---|---|
|
ac4fa5eb85
|
|||
|
8857718709
|
|||
|
dd17b2b9c1
|
|||
|
848439f664
|
|||
|
f83117424d
|
|||
|
8c19c32c76
|
|||
|
d62b2bed80
|
|||
|
5a23eb1768
|
|||
|
5f6e4763d3
|
|||
|
580c36fb89
|
|||
|
6c25af3b30
|
|||
|
a1da72d447
|
|||
|
ab4120cc22
|
|||
|
52eff0fa25
|
|||
|
beeb28abf2
|
|||
|
c0ab3837ac
|
|||
|
59d38c0d8d
|
|||
|
bd2247ce86
|
|||
|
da2d3f7f17
|
|||
|
7497b77384
|
@@ -1309,5 +1309,15 @@
|
|||||||
"presenceTypeGaming": "Playing",
|
"presenceTypeGaming": "Playing",
|
||||||
"presenceTypeMusic": "Listening to Music",
|
"presenceTypeMusic": "Listening to Music",
|
||||||
"presenceTypeWorkout": "Working out",
|
"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": "签到",
|
"checkIn": "签到",
|
||||||
"checkInNone": "尚未签到",
|
"checkInNone": "尚未签到",
|
||||||
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
||||||
"checkInResultLevel0": "最差运气",
|
"checkInResultLevel0": "大凶",
|
||||||
"checkInResultLevel1": "坏运气",
|
"checkInResultLevel1": "凶",
|
||||||
"checkInResultLevel2": "一个普通的日常",
|
"checkInResultLevel2": "中平",
|
||||||
"checkInResultLevel3": "好运",
|
"checkInResultLevel3": "吉",
|
||||||
"checkInResultLevel4": "最佳运气",
|
"checkInResultLevel4": "大吉",
|
||||||
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
||||||
"eventCalander": "活动日历",
|
"eventCalander": "活动日历",
|
||||||
"eventCalanderEmpty": "该日无活动。",
|
"eventCalanderEmpty": "该日无活动。",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import UserNotifications
|
|||||||
import Intents
|
import Intents
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
import KingfisherWebP
|
||||||
|
|
||||||
enum ParseNotificationPayloadError: Error {
|
enum ParseNotificationPayloadError: Error {
|
||||||
case missingMetadata(String)
|
case missingMetadata(String)
|
||||||
@@ -24,6 +25,11 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
_ request: UNNotificationRequest,
|
_ request: UNNotificationRequest,
|
||||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
||||||
) {
|
) {
|
||||||
|
KingfisherManager.shared.defaultOptions += [
|
||||||
|
.processor(WebPProcessor.default),
|
||||||
|
.cacheSerializer(WebPSerializer.default)
|
||||||
|
]
|
||||||
|
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
@@ -64,11 +70,39 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown)
|
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) {
|
if let pfpUrl = pfpUrl, let url = URL(string: pfpUrl) {
|
||||||
let targetSize = 512
|
let targetSize = 512
|
||||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
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?
|
var image: Data?
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let value):
|
case .success(let value):
|
||||||
@@ -76,37 +110,10 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("Unable to get pfp url: \(error)")
|
print("Unable to get pfp url: \(error)")
|
||||||
}
|
}
|
||||||
|
completeNotificationProcessing(image)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let sender = INPerson(
|
completeNotificationProcessing(nil)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ part 'poll.g.dart';
|
|||||||
@freezed
|
@freezed
|
||||||
sealed class SnPollWithStats with _$SnPollWithStats {
|
sealed class SnPollWithStats with _$SnPollWithStats {
|
||||||
const factory SnPollWithStats({
|
const factory SnPollWithStats({
|
||||||
required Map<String, dynamic>? userAnswer,
|
required SnPollAnswer? userAnswer,
|
||||||
@Default({}) Map<String, dynamic> stats,
|
@Default({}) Map<String, dynamic> stats,
|
||||||
required String id,
|
required String id,
|
||||||
required List<SnPollQuestion> questions,
|
required List<SnPollQuestion> questions,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnPollWithStats {
|
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
|
/// Create a copy of SnPollWithStats
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -28,12 +28,12 @@ $SnPollWithStatsCopyWith<SnPollWithStats> get copyWith => _$SnPollWithStatsCopyW
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -48,11 +48,11 @@ abstract mixin class $SnPollWithStatsCopyWith<$Res> {
|
|||||||
factory $SnPollWithStatsCopyWith(SnPollWithStats value, $Res Function(SnPollWithStats) _then) = _$SnPollWithStatsCopyWithImpl;
|
factory $SnPollWithStatsCopyWith(SnPollWithStats value, $Res Function(SnPollWithStats) _then) = _$SnPollWithStatsCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$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
|
/// @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,}) {
|
@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(
|
return _then(_self.copyWith(
|
||||||
userAnswer: freezed == userAnswer ? _self.userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
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 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 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
|
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?,
|
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) {
|
switch (_that) {
|
||||||
case _SnPollWithStats() when $default != null:
|
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 _:
|
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) {
|
switch (_that) {
|
||||||
case _SnPollWithStats():
|
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);}
|
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) {
|
switch (_that) {
|
||||||
case _SnPollWithStats() when $default != null:
|
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 _:
|
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()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _SnPollWithStats implements SnPollWithStats {
|
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);
|
factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
|
||||||
|
|
||||||
final Map<String, dynamic>? _userAnswer;
|
@override final SnPollAnswer? 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, dynamic> _stats;
|
final Map<String, dynamic> _stats;
|
||||||
@override@JsonKey() Map<String, dynamic> get stats {
|
@override@JsonKey() Map<String, dynamic> get stats {
|
||||||
if (_stats is EqualUnmodifiableMapView) return _stats;
|
if (_stats is EqualUnmodifiableMapView) return _stats;
|
||||||
@@ -261,12 +265,12 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -281,11 +285,11 @@ abstract mixin class _$SnPollWithStatsCopyWith<$Res> implements $SnPollWithStats
|
|||||||
factory _$SnPollWithStatsCopyWith(_SnPollWithStats value, $Res Function(_SnPollWithStats) _then) = __$SnPollWithStatsCopyWithImpl;
|
factory _$SnPollWithStatsCopyWith(_SnPollWithStats value, $Res Function(_SnPollWithStats) _then) = __$SnPollWithStatsCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$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
|
/// @nodoc
|
||||||
@@ -300,8 +304,8 @@ class __$SnPollWithStatsCopyWithImpl<$Res>
|
|||||||
/// with the given fields replaced by the non-null parameter values.
|
/// 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,}) {
|
@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(
|
return _then(_SnPollWithStats(
|
||||||
userAnswer: freezed == userAnswer ? _self._userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
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 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 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
|
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 _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
||||||
_SnPollWithStats(
|
_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 {},
|
stats: json['stats'] as Map<String, dynamic>? ?? const {},
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
questions:
|
questions:
|
||||||
@@ -32,7 +37,7 @@ _SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
|||||||
|
|
||||||
Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
|
Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'user_answer': instance.userAnswer,
|
'user_answer': instance.userAnswer?.toJson(),
|
||||||
'stats': instance.stats,
|
'stats': instance.stats,
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'questions': instance.questions.map((e) => e.toJson()).toList(),
|
'questions': instance.questions.map((e) => e.toJson()).toList(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'activity_rpc.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$presenceActivitiesHash() =>
|
String _$presenceActivitiesHash() =>
|
||||||
r'dcea3cad01b4010c0087f5281413d83a754c2a17';
|
r'3bfaa638eeb961ecd62a32d6a7760a6a7e7bf6f2';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/screens/about.dart';
|
import 'package:island/screens/about.dart';
|
||||||
import 'package:island/screens/developers/app_detail.dart';
|
import 'package:island/screens/developers/app_detail.dart';
|
||||||
import 'package:island/screens/developers/bot_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/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/edit_project.dart';
|
||||||
import 'package:island/screens/developers/new_project.dart';
|
import 'package:island/screens/developers/new_project.dart';
|
||||||
import 'package:island/screens/discovery/articles.dart';
|
import 'package:island/screens/discovery/articles.dart';
|
||||||
@@ -570,25 +566,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return const SizedBox.shrink(); // Temporary placeholder
|
return const SizedBox.shrink(); // Temporary placeholder
|
||||||
},
|
},
|
||||||
routes: [
|
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(
|
GoRoute(
|
||||||
name: 'developerAppDetail',
|
name: 'developerAppDetail',
|
||||||
path: 'apps/:appId',
|
path: 'apps/:appId',
|
||||||
@@ -599,15 +576,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
appId: state.pathParameters['appId']!,
|
appId: state.pathParameters['appId']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'developerBotNew',
|
|
||||||
path: 'bots/new',
|
|
||||||
builder:
|
|
||||||
(context, state) => NewBotScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'developerBotDetail',
|
name: 'developerBotDetail',
|
||||||
path: 'bots/:botId',
|
path: 'bots/:botId',
|
||||||
@@ -618,16 +586,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
botId: state.pathParameters['botId']!,
|
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']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "dart:async";
|
import "dart:async";
|
||||||
|
import "dart:math" as math;
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:file_picker/file_picker.dart";
|
import "package:file_picker/file_picker.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
@@ -140,6 +141,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final messageController = useTextEditingController();
|
final messageController = useTextEditingController();
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
|
|
||||||
|
// Scroll animation notifiers
|
||||||
|
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||||
|
|
||||||
final messageReplyingTo = useState<SnChatMessage?>(null);
|
final messageReplyingTo = useState<SnChatMessage?>(null);
|
||||||
final messageForwardingTo = useState<SnChatMessage?>(null);
|
final messageForwardingTo = useState<SnChatMessage?>(null);
|
||||||
final messageEditingTo = useState<SnChatMessage?>(null);
|
final messageEditingTo = useState<SnChatMessage?>(null);
|
||||||
@@ -164,6 +168,12 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
messagesNotifier.loadMore().then((_) => isLoading = false);
|
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);
|
scrollController.addListener(onScroll);
|
||||||
@@ -589,7 +599,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
listController: listController,
|
listController: listController,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 16,
|
top: 16,
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
bottom:
|
||||||
|
MediaQuery.of(context).padding.bottom +
|
||||||
|
80, // Leave space for chat input
|
||||||
),
|
),
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
reverse: true, // Show newest messages at the bottom
|
reverse: true, // Show newest messages at the bottom
|
||||||
@@ -828,7 +840,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Messages and Input in Column
|
// Messages only in Column
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
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
|
// Selection mode toolbar
|
||||||
if (isSelectionMode.value)
|
if (isSelectionMode.value)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/custom_app.dart';
|
import 'package:island/models/custom_app.dart';
|
||||||
import 'package:island/pods/network.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/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.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:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -68,12 +73,18 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'developerAppNew',
|
context: context,
|
||||||
pathParameters: {
|
isScrollControlled: true,
|
||||||
'name': publisherName,
|
builder:
|
||||||
'projectId': projectId,
|
(context) => SheetScaffold(
|
||||||
},
|
titleText: 'createCustomApp'.tr(),
|
||||||
|
child: NewCustomAppScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
isModal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
@@ -83,18 +94,44 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return RefreshIndicator(
|
return ExtendedRefreshIndicator(
|
||||||
onRefresh:
|
onRefresh:
|
||||||
() => ref.refresh(
|
() => ref.refresh(
|
||||||
customAppsProvider(publisherName, projectId).future,
|
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(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.only(top: 4),
|
padding: EdgeInsets.zero,
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final app = data[index];
|
final app = data[index];
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.all(8.0),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -138,7 +175,10 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
app.slug,
|
app.slug,
|
||||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.only(left: 20, right: 12),
|
contentPadding: EdgeInsets.only(
|
||||||
|
left: 20,
|
||||||
|
right: 12,
|
||||||
|
),
|
||||||
trailing: PopupMenuButton(
|
trailing: PopupMenuButton(
|
||||||
itemBuilder:
|
itemBuilder:
|
||||||
(context) => [
|
(context) => [
|
||||||
@@ -163,7 +203,9 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'delete',
|
'delete',
|
||||||
style: TextStyle(color: Colors.red),
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -171,13 +213,19 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 'edit') {
|
if (value == 'edit') {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'developerAppEdit',
|
context: context,
|
||||||
pathParameters: {
|
isScrollControlled: true,
|
||||||
'name': publisherName,
|
builder:
|
||||||
'projectId': projectId,
|
(context) => SheetScaffold(
|
||||||
'id': app.id,
|
titleText: 'editCustomApp'.tr(),
|
||||||
},
|
child: EditAppScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
id: app.id,
|
||||||
|
isModal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else if (value == 'delete') {
|
} else if (value == 'delete') {
|
||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
@@ -185,7 +233,9 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
'deleteCustomApp'.tr(),
|
'deleteCustomApp'.tr(),
|
||||||
).then((confirm) {
|
).then((confirm) {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(
|
||||||
|
apiClientProvider,
|
||||||
|
);
|
||||||
client.delete(
|
client.delete(
|
||||||
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
|
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
|
||||||
);
|
);
|
||||||
@@ -207,6 +257,9 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/bot.dart';
|
import 'package:island/models/bot.dart';
|
||||||
import 'package:island/pods/network.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/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
part 'bots.g.dart';
|
part 'bots.g.dart';
|
||||||
|
|
||||||
@@ -46,12 +51,18 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'developerBotNew',
|
context: context,
|
||||||
pathParameters: {
|
isScrollControlled: true,
|
||||||
'name': publisherName,
|
builder:
|
||||||
'projectId': projectId,
|
(context) => SheetScaffold(
|
||||||
},
|
titleText: 'createBot'.tr(),
|
||||||
|
child: NewBotScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
isModal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
@@ -64,13 +75,39 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
return ExtendedRefreshIndicator(
|
return ExtendedRefreshIndicator(
|
||||||
onRefresh:
|
onRefresh:
|
||||||
() => ref.refresh(botsProvider(publisherName, projectId).future),
|
() => 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(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: EdgeInsets.zero,
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final bot = data[index];
|
final bot = data[index];
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.all(8.0),
|
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||||
@@ -102,7 +139,10 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
value: 'delete',
|
value: 'delete',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.delete, color: Colors.red),
|
const Icon(
|
||||||
|
Symbols.delete,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'delete',
|
'delete',
|
||||||
@@ -114,13 +154,19 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 'edit') {
|
if (value == 'edit') {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'developerBotEdit',
|
context: context,
|
||||||
pathParameters: {
|
isScrollControlled: true,
|
||||||
'name': publisherName,
|
builder:
|
||||||
'projectId': projectId,
|
(context) => SheetScaffold(
|
||||||
'id': bot.id,
|
titleText: 'editBot'.tr(),
|
||||||
},
|
child: EditBotScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
id: bot.id,
|
||||||
|
isModal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else if (value == 'delete') {
|
} else if (value == 'delete') {
|
||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
@@ -154,6 +200,9 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
|||||||
@@ -39,11 +39,13 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
final String publisherName;
|
final String publisherName;
|
||||||
final String projectId;
|
final String projectId;
|
||||||
final String? id;
|
final String? id;
|
||||||
|
final bool isModal;
|
||||||
const EditAppScreen({
|
const EditAppScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.publisherName,
|
required this.publisherName,
|
||||||
required this.projectId,
|
required this.projectId,
|
||||||
this.id,
|
this.id,
|
||||||
|
this.isModal = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -177,7 +179,12 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: scopeController,
|
controller: scopeController,
|
||||||
decoration: InputDecoration(labelText: 'scopeName'.tr()),
|
decoration: InputDecoration(
|
||||||
|
labelText: 'scopeName'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
@@ -220,6 +227,9 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
hintText: 'https://example.com/auth/callback',
|
hintText: 'https://example.com/auth/callback',
|
||||||
helperText: 'redirectUriHint'.tr(),
|
helperText: 'redirectUriHint'.tr(),
|
||||||
helperMaxLines: 3,
|
helperMaxLines: 3,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@@ -316,12 +326,7 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
final bodyContent =
|
||||||
isNoBackground: false,
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
|
||||||
),
|
|
||||||
body:
|
|
||||||
app == null && !isNew
|
app == null && !isNew
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: app?.hasError == true && !isNew
|
: app?.hasError == true && !isNew
|
||||||
@@ -382,11 +387,17 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
decoration: InputDecoration(
|
||||||
|
labelText: 'name'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) =>
|
(_) =>
|
||||||
FocusManager.instance.primaryFocus
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -394,11 +405,15 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'slug'.tr(),
|
labelText: 'slug'.tr(),
|
||||||
helperText: 'slugHint'.tr(),
|
helperText: 'slugHint'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) =>
|
(_) =>
|
||||||
FocusManager.instance.primaryFocus
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -406,12 +421,16 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'description'.tr(),
|
labelText: 'description'.tr(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) =>
|
(_) =>
|
||||||
FocusManager.instance.primaryFocus
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ExpansionPanelList(
|
ExpansionPanelList(
|
||||||
@@ -438,6 +457,11 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'homePageUrl'.tr(),
|
labelText: 'homePageUrl'.tr(),
|
||||||
hintText: 'https://example.com',
|
hintText: 'https://example.com',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
),
|
),
|
||||||
@@ -446,6 +470,11 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'privacyPolicyUrl'.tr(),
|
labelText: 'privacyPolicyUrl'.tr(),
|
||||||
hintText: 'https://example.com/privacy',
|
hintText: 'https://example.com/privacy',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
),
|
),
|
||||||
@@ -454,6 +483,11 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'termsOfServiceUrl'.tr(),
|
labelText: 'termsOfServiceUrl'.tr(),
|
||||||
hintText: 'https://example.com/terms',
|
hintText: 'https://example.com/terms',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
),
|
),
|
||||||
@@ -463,9 +497,8 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
ExpansionPanel(
|
ExpansionPanel(
|
||||||
headerBuilder:
|
headerBuilder:
|
||||||
(context, isExpanded) => ListTile(
|
(context, isExpanded) =>
|
||||||
title: Text('oauthConfig').tr(),
|
ListTile(title: Text('oauthConfig').tr()),
|
||||||
),
|
|
||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -480,15 +513,11 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
(uri) => ListTile(
|
(uri) => ListTile(
|
||||||
title: Text(uri),
|
title: Text(uri),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(Symbols.delete),
|
||||||
Symbols.delete,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
redirectUris.value =
|
redirectUris.value =
|
||||||
redirectUris.value
|
redirectUris.value
|
||||||
.where(
|
.where((u) => u != uri)
|
||||||
(u) => u != uri,
|
|
||||||
)
|
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -501,8 +530,9 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
title: Text('addRedirectUri'.tr()),
|
title: Text('addRedirectUri'.tr()),
|
||||||
onTap: showAddRedirectUriDialog,
|
onTap: showAddRedirectUriDialog,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius:
|
borderRadius: BorderRadius.circular(
|
||||||
BorderRadius.circular(8),
|
8,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -520,9 +550,7 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
(scope) => ListTile(
|
(scope) => ListTile(
|
||||||
title: Text(scope),
|
title: Text(scope),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(Symbols.delete),
|
||||||
Symbols.delete,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
allowedScopes.value =
|
allowedScopes.value =
|
||||||
allowedScopes.value
|
allowedScopes.value
|
||||||
@@ -568,8 +596,7 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed:
|
onPressed: submitting.value ? null : performAction,
|
||||||
submitting.value ? null : performAction,
|
|
||||||
label: Text('saveChanges'.tr()),
|
label: Text('saveChanges'.tr()),
|
||||||
icon: const Icon(Symbols.save),
|
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 publisherName;
|
||||||
final String projectId;
|
final String projectId;
|
||||||
final String? id;
|
final String? id;
|
||||||
|
final bool isModal;
|
||||||
const EditBotScreen({
|
const EditBotScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.publisherName,
|
required this.publisherName,
|
||||||
required this.projectId,
|
required this.projectId,
|
||||||
this.id,
|
this.id,
|
||||||
|
this.isModal = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -191,10 +193,7 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
final bodyContent =
|
||||||
isNoBackground: false,
|
|
||||||
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
|
||||||
body:
|
|
||||||
botData == null && !isNew
|
botData == null && !isNew
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: botData?.hasError == true && !isNew
|
: botData?.hasError == true && !isNew
|
||||||
@@ -255,7 +254,14 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
decoration: InputDecoration(
|
||||||
|
labelText: 'name'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -263,6 +269,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'nickname'.tr(),
|
labelText: 'nickname'.tr(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -271,6 +282,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'slug'.tr(),
|
labelText: 'slug'.tr(),
|
||||||
helperText: 'slugHint'.tr(),
|
helperText: 'slugHint'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -279,6 +295,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'bio'.tr(),
|
labelText: 'bio'.tr(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
@@ -291,6 +312,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
controller: firstNameController,
|
controller: firstNameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'firstName'.tr(),
|
labelText: 'firstName'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -299,6 +325,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
controller: middleNameController,
|
controller: middleNameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'middleName'.tr(),
|
labelText: 'middleName'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -307,6 +338,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
controller: lastNameController,
|
controller: lastNameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'lastName'.tr(),
|
labelText: 'lastName'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -321,6 +357,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
controller: genderController,
|
controller: genderController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'gender'.tr(),
|
labelText: 'gender'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -329,6 +370,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
controller: pronounsController,
|
controller: pronounsController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'pronouns'.tr(),
|
labelText: 'pronouns'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -343,6 +389,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
controller: locationController,
|
controller: locationController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'location'.tr(),
|
labelText: 'location'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -351,6 +402,11 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
controller: timeZoneController,
|
controller: timeZoneController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'timeZone'.tr(),
|
labelText: 'timeZone'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -370,13 +426,14 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border.all(
|
||||||
bottom: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -403,8 +460,7 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed:
|
onPressed: submitting.value ? null : performAction,
|
||||||
submitting.value ? null : performAction,
|
|
||||||
label: Text('saveChanges').tr(),
|
label: Text('saveChanges').tr(),
|
||||||
icon: const Icon(Symbols.save),
|
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:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -99,15 +97,6 @@ class DeveloperHubScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
if (currentProject.value == null)
|
|
||||||
...([
|
|
||||||
// Welcome Section
|
|
||||||
_WelcomeSection(currentDeveloper: currentDeveloper.value),
|
|
||||||
|
|
||||||
// Navigation Tabs
|
|
||||||
_NavigationTabs(),
|
|
||||||
]),
|
|
||||||
|
|
||||||
// Main Content
|
// Main Content
|
||||||
if (currentProject.value != null)
|
if (currentProject.value != null)
|
||||||
Expanded(
|
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
|
// Main Content Section
|
||||||
class _MainContentSection extends HookConsumerWidget {
|
class _MainContentSection extends HookConsumerWidget {
|
||||||
final SnDeveloper? currentDeveloper;
|
final SnDeveloper? currentDeveloper;
|
||||||
|
|||||||
@@ -4,10 +4,20 @@ import 'package:island/screens/developers/edit_app.dart';
|
|||||||
class NewCustomAppScreen extends StatelessWidget {
|
class NewCustomAppScreen extends StatelessWidget {
|
||||||
final String publisherName;
|
final String publisherName;
|
||||||
final String projectId;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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:flutter/material.dart';
|
||||||
import 'package:island/screens/developers/edit_bot.dart';
|
import 'package:island/screens/developers/edit_bot.dart';
|
||||||
|
|
||||||
class NewBotScreen extends StatelessWidget {
|
class NewBotScreen extends StatelessWidget {
|
||||||
final String publisherName;
|
final String publisherName;
|
||||||
final String projectId;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/dev_project.dart';
|
import 'package:island/models/dev_project.dart';
|
||||||
import 'package:island/screens/developers/apps.dart';
|
import 'package:island/screens/developers/apps.dart';
|
||||||
import 'package:island/screens/developers/bots.dart';
|
import 'package:island/screens/developers/bots.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class ProjectDetailView extends HookConsumerWidget {
|
class ProjectDetailView extends HookConsumerWidget {
|
||||||
final String publisherName;
|
final String publisherName;
|
||||||
@@ -54,6 +56,37 @@ class ProjectDetailView extends HookConsumerWidget {
|
|||||||
label: Text('bots'.tr()),
|
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 {
|
} else {
|
||||||
@@ -97,7 +131,7 @@ class ProjectDetailView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
BotsScreen(publisherName: publisherName, projectId: project.id),
|
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||||
],
|
],
|
||||||
),
|
).padding(horizontal: 8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -737,7 +737,7 @@ class ActivityListNotifier extends _$ActivityListNotifier
|
|||||||
};
|
};
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/sphere/activities',
|
'/sphere/timeline',
|
||||||
queryParameters: queryParameters,
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'explore.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$activityListNotifierHash() =>
|
String _$activityListNotifierHash() =>
|
||||||
r'a3ad3242f08139bef14a2f0fab6591ce8b3cb9f0';
|
r'77ffc7852feffa5438b56fa26123d453b7c310cf';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.add_circle,
|
Symbols.remove_circle,
|
||||||
),
|
),
|
||||||
label: Text('unsubscribe'.tr()),
|
label: Text('unsubscribe'.tr()),
|
||||||
)
|
)
|
||||||
@@ -214,7 +214,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.remove_circle,
|
Symbols.add_circle,
|
||||||
),
|
),
|
||||||
label: Text('subscribe'.tr()),
|
label: Text('subscribe'.tr()),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -326,14 +326,34 @@ class _PublisherHeatmapWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class _PublisherCategoryTabWidget extends StatelessWidget {
|
class _PublisherCategoryTabWidget extends StatelessWidget {
|
||||||
final TabController categoryTabController;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: TabBar(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TabBar(
|
||||||
controller: categoryTabController,
|
controller: categoryTabController,
|
||||||
dividerColor: Colors.transparent,
|
dividerColor: Colors.transparent,
|
||||||
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
@@ -343,6 +363,228 @@ class _PublisherCategoryTabWidget extends StatelessWidget {
|
|||||||
Tab(text: 'postArticle'.tr()),
|
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;
|
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 subscribing = useState(false);
|
||||||
|
final isPinnedExpanded = useState(true);
|
||||||
|
|
||||||
Future<void> subscribe() async {
|
Future<void> subscribe() async {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
@@ -494,21 +747,66 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverGap(16),
|
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),
|
SliverPostList(pubName: name, pinned: true),
|
||||||
|
],
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _PublisherCategoryTabWidget(
|
child: _PublisherCategoryTabWidget(
|
||||||
categoryTabController: categoryTabController,
|
categoryTabController: categoryTabController,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
orderDesc: orderDesc,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
showAdvancedFilters: showAdvancedFilters,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPostList(
|
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,
|
pubName: name,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
type: switch (categoryTab.value) {
|
type:
|
||||||
1 => 0,
|
categoryTab.value == 1
|
||||||
2 => 1,
|
? 0
|
||||||
_ => null,
|
: (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(
|
SliverGap(
|
||||||
MediaQuery.of(context).padding.bottom + 16,
|
MediaQuery.of(context).padding.bottom + 16,
|
||||||
@@ -617,21 +915,60 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
heatmap: heatmap,
|
heatmap: heatmap,
|
||||||
).padding(vertical: 4),
|
).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),
|
SliverPostList(pubName: name, pinned: true),
|
||||||
|
],
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _PublisherCategoryTabWidget(
|
child: _PublisherCategoryTabWidget(
|
||||||
categoryTabController: categoryTabController,
|
categoryTabController: categoryTabController,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
orderDesc: orderDesc,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
showAdvancedFilters: showAdvancedFilters,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPostList(
|
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,
|
pubName: name,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
type: switch (categoryTab.value) {
|
type:
|
||||||
1 => 0,
|
categoryTab.value == 1
|
||||||
2 => 1,
|
? 0
|
||||||
_ => null,
|
: (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),
|
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
|
import "dart:math" as math;
|
||||||
import "package:dio/dio.dart";
|
import "package:dio/dio.dart";
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
@@ -57,6 +58,9 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
final listController = useMemoized(() => ListController(), []);
|
||||||
|
|
||||||
|
// Scroll animation notifiers
|
||||||
|
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||||
|
|
||||||
// Update local thoughts when provider data changes
|
// Update local thoughts when provider data changes
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
thoughts.whenData((data) {
|
thoughts.whenData((data) {
|
||||||
@@ -86,6 +90,20 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [localThoughts.value.length, isStreaming.value]);
|
}, [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 {
|
void sendMessage() async {
|
||||||
if (messageController.text.trim().isEmpty) return;
|
if (messageController.text.trim().isEmpty) return;
|
||||||
|
|
||||||
@@ -258,7 +276,10 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// Thoughts list
|
||||||
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -269,7 +290,12 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
(thoughtList) => SuperListView.builder(
|
(thoughtList) => SuperListView.builder(
|
||||||
listController: listController,
|
listController: listController,
|
||||||
controller: scrollController,
|
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,
|
reverse: true,
|
||||||
itemCount:
|
itemCount:
|
||||||
localThoughts.value.length +
|
localThoughts.value.length +
|
||||||
@@ -293,7 +319,8 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
loading:
|
loading:
|
||||||
() => const Center(child: CircularProgressIndicator()),
|
() =>
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
error:
|
error:
|
||||||
(error, _) => ResponseErrorWidget(
|
(error, _) => ResponseErrorWidget(
|
||||||
error: error,
|
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:convert";
|
||||||
|
import "dart:math" as math;
|
||||||
import "package:dio/dio.dart";
|
import "package:dio/dio.dart";
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
@@ -54,6 +55,9 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
final listController = useMemoized(() => ListController(), []);
|
||||||
|
|
||||||
|
// Scroll animation notifiers
|
||||||
|
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||||
|
|
||||||
// Scroll to bottom when thoughts change or streaming state changes
|
// Scroll to bottom when thoughts change or streaming state changes
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (localThoughts.value.isNotEmpty || isStreaming.value) {
|
if (localThoughts.value.isNotEmpty || isStreaming.value) {
|
||||||
@@ -68,6 +72,20 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [localThoughts.value.length, isStreaming.value]);
|
}, [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 {
|
void sendMessage() async {
|
||||||
if (messageController.text.trim().isEmpty) return;
|
if (messageController.text.trim().isEmpty) return;
|
||||||
|
|
||||||
@@ -196,7 +214,10 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: currentTopic.value ?? 'aiThought'.tr(),
|
titleText: currentTopic.value ?? 'aiThought'.tr(),
|
||||||
child: Center(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Thoughts list
|
||||||
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -205,10 +226,16 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
child: SuperListView.builder(
|
child: SuperListView.builder(
|
||||||
listController: listController,
|
listController: listController,
|
||||||
controller: scrollController,
|
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,
|
reverse: true,
|
||||||
itemCount:
|
itemCount:
|
||||||
localThoughts.value.length + (isStreaming.value ? 1 : 0),
|
localThoughts.value.length +
|
||||||
|
(isStreaming.value ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (isStreaming.value && index == 0) {
|
if (isStreaming.value && index == 0) {
|
||||||
return ThoughtItem(
|
return ThoughtItem(
|
||||||
@@ -218,7 +245,8 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
streamingFunctionCalls: functionCalls.value,
|
streamingFunctionCalls: functionCalls.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final thoughtIndex = isStreaming.value ? index - 1 : index;
|
final thoughtIndex =
|
||||||
|
isStreaming.value ? index - 1 : index;
|
||||||
final thought = localThoughts.value[thoughtIndex];
|
final thought = localThoughts.value[thoughtIndex];
|
||||||
return ThoughtItem(
|
return ThoughtItem(
|
||||||
thought: thought,
|
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,
|
messageController: messageController,
|
||||||
isStreaming: isStreaming.value,
|
isStreaming: isStreaming.value,
|
||||||
onSend: sendMessage,
|
onSend: sendMessage,
|
||||||
attachedMessages: attachedMessages,
|
attachedMessages: attachedMessages,
|
||||||
attachedPosts: attachedPosts,
|
attachedPosts: attachedPosts,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,20 +106,6 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
|
|||||||
child: NotificationCard(notification: notification),
|
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: () {},
|
onDismissed: () {},
|
||||||
dismissType: DismissType.onSwipe,
|
dismissType: DismissType.onSwipe,
|
||||||
displayDuration: const Duration(seconds: 5),
|
displayDuration: const Duration(seconds: 5),
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/route.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class NotificationCard extends HookConsumerWidget {
|
class NotificationCard extends HookConsumerWidget {
|
||||||
final SnNotification notification;
|
final SnNotification notification;
|
||||||
@@ -14,9 +17,28 @@ class NotificationCard extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final icon = Symbols.info;
|
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,
|
elevation: 4,
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
),
|
),
|
||||||
@@ -67,6 +89,7 @@ class NotificationCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter_blurhash/flutter_blurhash.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/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:gal/gal.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:island/widgets/content/cloud_files.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:island/widgets/content/sensitive.dart';
|
||||||
import 'package:material_symbols_icons/symbols.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:styled_widget/styled_widget.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@@ -47,13 +38,100 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
double calculateAspectRatio() {
|
double calculateAspectRatio() {
|
||||||
double total = 0;
|
final ratios = <double>[];
|
||||||
for (var ratio in files.map((e) => e.fileMeta?['ratio'] ?? 1)) {
|
|
||||||
if (ratio is double) total += ratio;
|
// Collect all valid ratios
|
||||||
if (ratio is String) total += double.parse(ratio);
|
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
|
@override
|
||||||
@@ -90,7 +168,7 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (!disableZoomIn) {
|
if (!disableZoomIn) {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
CloudFileZoomIn(item: file, heroTag: heroTags[i]),
|
CloudFileLightbox(item: file, heroTag: heroTags[i]),
|
||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -151,7 +229,7 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (!disableZoomIn) {
|
if (!disableZoomIn) {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
CloudFileZoomIn(item: files.first, heroTag: heroTags.first),
|
CloudFileLightbox(item: files.first, heroTag: heroTags.first),
|
||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -169,10 +247,7 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
child:
|
child:
|
||||||
isAudio
|
isAudio
|
||||||
? widgetItem
|
? widgetItem
|
||||||
: AspectRatio(
|
: IntrinsicWidth(child: IntrinsicHeight(child: widgetItem)),
|
||||||
aspectRatio: calculateAspectRatio(),
|
|
||||||
child: widgetItem,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,15 +263,20 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
aspectRatio: calculateAspectRatio(),
|
aspectRatio: calculateAspectRatio(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: padding ?? EdgeInsets.zero,
|
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,
|
itemSnapping: true,
|
||||||
itemExtent: math.min(
|
itemExtent: itemExtent,
|
||||||
math.min(
|
|
||||||
MediaQuery.of(context).size.width * 0.75,
|
|
||||||
maxWidth * 0.75,
|
|
||||||
),
|
|
||||||
640,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -231,11 +311,13 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (!disableZoomIn) {
|
if (!disableZoomIn) {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
|
CloudFileLightbox(item: files[i], heroTag: heroTags[i]),
|
||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -273,7 +355,7 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (!disableZoomIn) {
|
if (!disableZoomIn) {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
CloudFileZoomIn(
|
CloudFileLightbox(
|
||||||
item: files[index],
|
item: files[index],
|
||||||
heroTag: heroTags[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 {
|
class _CloudFileListEntry extends HookConsumerWidget {
|
||||||
final SnCloudFile file;
|
final SnCloudFile file;
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
@@ -535,15 +412,8 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
final lockedByDS = dataSaving && !showDataSaving.value;
|
final lockedByDS = dataSaving && !showDataSaving.value;
|
||||||
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
||||||
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
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();
|
Widget bg = const SizedBox.shrink();
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
@@ -551,7 +421,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
bg = BlurHash(hash: meta['blur'] as String);
|
bg = BlurHash(hash: meta['blur'] as String);
|
||||||
} else if (!lockedByDS && !lockedByMature) {
|
} else if (!lockedByDS && !lockedByMature) {
|
||||||
bg = ImageFiltered(
|
bg = ImageFiltered(
|
||||||
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
imageFilter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||||
child: CloudFileWidget(
|
child: CloudFileWidget(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
item: file,
|
item: file,
|
||||||
@@ -581,7 +451,9 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
fit: fit,
|
fit: fit,
|
||||||
useInternalGate: false,
|
useInternalGate: false,
|
||||||
))
|
))
|
||||||
: AspectRatio(aspectRatio: ratio, child: const SizedBox.shrink());
|
: IntrinsicWidth(
|
||||||
|
child: IntrinsicHeight(child: const SizedBox.shrink()),
|
||||||
|
);
|
||||||
|
|
||||||
Widget overlays;
|
Widget overlays;
|
||||||
if (lockedByDS) {
|
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) {
|
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,
|
aspectRatio: ratio,
|
||||||
child:
|
child:
|
||||||
(useInternalGate && dataSaving && !unlocked.value)
|
(useInternalGate && dataSaving && !unlocked.value)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:island/models/embed.dart';
|
import 'package:island/models/embed.dart';
|
||||||
import 'package:island/models/poll.dart';
|
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/utils/mapping.dart';
|
import 'package:island/utils/mapping.dart';
|
||||||
import 'package:island/widgets/content/embed/link.dart';
|
import 'package:island/widgets/content/embed/link.dart';
|
||||||
@@ -54,13 +53,10 @@ class EmbedListWidget extends StatelessWidget {
|
|||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
child:
|
child:
|
||||||
embedData['poll'] == null
|
embedData['id'] == null
|
||||||
? const Text('Poll was not loaded...')
|
? const Text('Poll was unavailable...')
|
||||||
: PollSubmit(
|
: PollSubmit(
|
||||||
initialAnswers:
|
pollId: embedData['id'],
|
||||||
embedData['poll']?['user_answer']?['answer'],
|
|
||||||
stats: embedData['poll']?['stats'],
|
|
||||||
poll: SnPollWithStats.fromJson(embedData['poll']),
|
|
||||||
onSubmit: (_) {},
|
onSubmit: (_) {},
|
||||||
isReadonly: !isInteractive,
|
isReadonly: !isInteractive,
|
||||||
isInitiallyExpanded: isFullPost,
|
isInitiallyExpanded: isFullPost,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/screens/account/profile.dart';
|
||||||
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/markdown_latex.dart';
|
import 'package:island/widgets/content/markdown_latex.dart';
|
||||||
@@ -397,7 +399,13 @@ class MentionChipSpanNode extends SpanNode {
|
|||||||
onTap: () => onTap(type, id),
|
onTap: () => onTap(type, id),
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
child: Container(
|
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(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor.withOpacity(0.1),
|
color: backgroundColor.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
@@ -411,18 +419,58 @@ class MentionChipSpanNode extends SpanNode {
|
|||||||
color: backgroundColor.withOpacity(0.5),
|
color: backgroundColor.withOpacity(0.5),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(32)),
|
borderRadius: const BorderRadius.all(Radius.circular(32)),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: switch (parts.length == 1 ? 'u' : parts.first) {
|
||||||
switch (parts.first.isEmpty ? '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,
|
'c' => Symbols.forum_rounded,
|
||||||
'r' => Symbols.group_rounded,
|
'r' => Symbols.group_rounded,
|
||||||
'u' => Symbols.person_rounded,
|
|
||||||
'p' => Symbols.edit_rounded,
|
|
||||||
_ => Symbols.person_rounded,
|
_ => Symbols.person_rounded,
|
||||||
},
|
}),
|
||||||
size: 14,
|
size: 14,
|
||||||
color: foregroundColor,
|
color: foregroundColor,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
).padding(all: 2),
|
).padding(all: 2),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
parts.last,
|
parts.last,
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/pods/network.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/alert.dart';
|
||||||
import 'package:island/widgets/poll/poll_stats_widget.dart';
|
import 'package:island/widgets/poll/poll_stats_widget.dart';
|
||||||
|
|
||||||
class PollSubmit extends ConsumerStatefulWidget {
|
class PollSubmit extends ConsumerStatefulWidget {
|
||||||
const PollSubmit({
|
const PollSubmit({
|
||||||
super.key,
|
super.key,
|
||||||
required this.poll,
|
required this.pollId,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
required this.stats,
|
|
||||||
this.initialAnswers,
|
this.initialAnswers,
|
||||||
this.onCancel,
|
this.onCancel,
|
||||||
this.showProgress = true,
|
this.showProgress = true,
|
||||||
@@ -20,14 +20,13 @@ class PollSubmit extends ConsumerStatefulWidget {
|
|||||||
this.isInitiallyExpanded = false,
|
this.isInitiallyExpanded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final SnPollWithStats poll;
|
final String pollId;
|
||||||
|
|
||||||
/// Callback when user submits all answers. Map questionId -> answer.
|
/// Callback when user submits all answers. Map questionId -> answer.
|
||||||
final void Function(Map<String, dynamic> answers) onSubmit;
|
final void Function(Map<String, dynamic> answers) onSubmit;
|
||||||
|
|
||||||
/// Optional initial answers, keyed by questionId.
|
/// Optional initial answers, keyed by questionId.
|
||||||
final Map<String, dynamic>? initialAnswers;
|
final Map<String, dynamic>? initialAnswers;
|
||||||
final Map<String, dynamic>? stats;
|
|
||||||
|
|
||||||
/// Optional cancel callback.
|
/// Optional cancel callback.
|
||||||
final VoidCallback? onCancel;
|
final VoidCallback? onCancel;
|
||||||
@@ -45,7 +44,7 @@ class PollSubmit extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PollSubmitState extends ConsumerState<PollSubmit> {
|
class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||||
late final List<SnPollQuestion> _questions;
|
List<SnPollQuestion>? _questions;
|
||||||
int _index = 0;
|
int _index = 0;
|
||||||
bool _submitting = false;
|
bool _submitting = false;
|
||||||
bool _isModifying = false; // New state to track if user is modifying answers
|
bool _isModifying = false; // New state to track if user is modifying answers
|
||||||
@@ -66,14 +65,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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 ?? {});
|
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
||||||
// Set initial collapse state based on the parameter
|
// Set initial collapse state based on the parameter
|
||||||
_isCollapsed = !widget.isInitiallyExpanded;
|
_isCollapsed = !widget.isInitiallyExpanded;
|
||||||
if (!widget.isReadonly) {
|
if (!widget.isReadonly) {
|
||||||
_loadCurrentIntoLocalState();
|
|
||||||
// If initial answers are provided, set _isModifying to false initially
|
// If initial answers are provided, set _isModifying to false initially
|
||||||
// so the "Modify" button is shown.
|
// so the "Modify" button is shown.
|
||||||
if (widget.initialAnswers != null && widget.initialAnswers!.isNotEmpty) {
|
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
|
@override
|
||||||
void didUpdateWidget(covariant PollSubmit oldWidget) {
|
void didUpdateWidget(covariant PollSubmit oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.poll.id != widget.poll.id) {
|
if (oldWidget.pollId != widget.pollId) {
|
||||||
_index = 0;
|
_index = 0;
|
||||||
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
||||||
_questions
|
// Reset modification state when poll changes
|
||||||
..clear()
|
|
||||||
..addAll(
|
|
||||||
[...widget.poll.questions]
|
|
||||||
..sort((a, b) => a.order.compareTo(b.order)),
|
|
||||||
);
|
|
||||||
if (!widget.isReadonly) {
|
|
||||||
_loadCurrentIntoLocalState();
|
|
||||||
// If poll ID changes, reset modification state
|
|
||||||
_isModifying = false;
|
_isModifying = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -108,7 +105,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
SnPollQuestion get _current => _questions[_index];
|
SnPollQuestion get _current => _questions![_index];
|
||||||
|
|
||||||
void _loadCurrentIntoLocalState() {
|
void _loadCurrentIntoLocalState() {
|
||||||
final q = _current;
|
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
|
// Persist current question before final submit
|
||||||
_persistCurrentAnswer();
|
_persistCurrentAnswer();
|
||||||
|
|
||||||
@@ -213,7 +210,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
final dio = ref.read(apiClientProvider);
|
final dio = ref.read(apiClientProvider);
|
||||||
|
|
||||||
await dio.post(
|
await dio.post(
|
||||||
'/sphere/polls/${widget.poll.id}/answer',
|
'/sphere/polls/${poll.id}/answer',
|
||||||
data: {'answer': _answers},
|
data: {'answer': _answers},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -233,17 +230,17 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _next() {
|
void _next(SnPollWithStats poll) {
|
||||||
if (_submitting) return;
|
if (_submitting) return;
|
||||||
_persistCurrentAnswer();
|
_persistCurrentAnswer();
|
||||||
if (_index < _questions.length - 1) {
|
if (_index < _questions!.length - 1) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_index++;
|
_index++;
|
||||||
_loadCurrentIntoLocalState();
|
_loadCurrentIntoLocalState();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Final submit to API
|
// 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;
|
final q = _current;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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 &&
|
if (widget.showProgress &&
|
||||||
_isModifying) // Only show progress when modifying
|
_isModifying) // Only show progress when modifying
|
||||||
Text(
|
Text(
|
||||||
'${_index + 1} / ${_questions.length}',
|
'${_index + 1} / ${_questions!.length}',
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -334,12 +305,18 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStats(BuildContext context, SnPollQuestion q) {
|
Widget _buildStats(
|
||||||
return PollStatsWidget(question: q, stats: widget.stats);
|
BuildContext context,
|
||||||
|
SnPollQuestion q,
|
||||||
|
Map<String, dynamic>? stats,
|
||||||
|
) {
|
||||||
|
return PollStatsWidget(question: q, stats: stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(BuildContext context) {
|
Widget _buildBody(BuildContext context, SnPollWithStats poll) {
|
||||||
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
|
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
|
return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying
|
||||||
}
|
}
|
||||||
final q = _current;
|
final q = _current;
|
||||||
@@ -449,11 +426,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNavBar(BuildContext context) {
|
Widget _buildNavBar(BuildContext context, SnPollWithStats poll) {
|
||||||
final isLast = _index == _questions.length - 1;
|
final isLast = _index == _questions!.length - 1;
|
||||||
final canProceed = _isCurrentAnswered() && !_submitting;
|
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
|
// If poll is submitted and not in modification mode, show "Modify" button
|
||||||
return FilledButton.icon(
|
return FilledButton.icon(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
@@ -498,32 +477,32 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
)
|
)
|
||||||
: Icon(isLast ? Icons.check : Icons.arrow_forward),
|
: Icon(isLast ? Icons.check : Icons.arrow_forward),
|
||||||
label: Text(isLast ? 'submit'.tr() : 'next'.tr()),
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null || widget.poll.description != null)
|
if (poll.title != null || poll.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title?.isNotEmpty ?? false)
|
if (poll.title?.isNotEmpty ?? false)
|
||||||
Text(
|
Text(
|
||||||
widget.poll.title!,
|
poll.title!,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
if (widget.poll.description?.isNotEmpty ?? false)
|
if (poll.description?.isNotEmpty ?? false)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.poll.description!,
|
poll.description!,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -534,7 +513,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (final q in _questions)
|
for (final q in _questions!)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: Column(
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null || widget.poll.description != null)
|
if (poll.title != null || poll.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null)
|
if (poll.title != null)
|
||||||
Text(
|
Text(
|
||||||
widget.poll.title!,
|
poll.title!,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
if (widget.poll.description != null)
|
if (poll.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.poll.description!,
|
poll.description!,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -612,7 +591,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (final q in _questions)
|
for (final q in _questions!)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: Column(
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -670,20 +649,20 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null)
|
if (poll.title != null)
|
||||||
Text(
|
Text(
|
||||||
widget.poll.title!,
|
poll.title!,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
if (widget.poll.description != null)
|
if (poll.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.poll.description!,
|
poll.description!,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -697,7 +676,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${_questions.length} question${_questions.length == 1 ? '' : 's'}',
|
'${_questions!.length} question${_questions!.length == 1 ? '' : 's'}',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -729,21 +708,48 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If collapsed, show collapsed view for all states
|
// If collapsed, show collapsed view for all states
|
||||||
if (_isCollapsed) {
|
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 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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildCollapsedView(context),
|
_buildCollapsedView(context, poll),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
@@ -751,8 +757,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
final offset = Tween<Offset>(
|
final offset = Tween<Offset>(
|
||||||
begin: const Offset(0, -0.1),
|
begin: const Offset(0, -0.1),
|
||||||
end: Offset.zero,
|
end: Offset.zero,
|
||||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
).animate(
|
||||||
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
CurvedAnimation(parent: anim, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
final fade = CurvedAnimation(
|
||||||
|
parent: anim,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
return FadeTransition(
|
return FadeTransition(
|
||||||
opacity: fade,
|
opacity: fade,
|
||||||
child: SlideTransition(position: offset, child: child),
|
child: SlideTransition(position: offset, child: child),
|
||||||
@@ -761,7 +772,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
key: const ValueKey('submitted_expanded'),
|
key: const ValueKey('submitted_expanded'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildCollapsedView(context),
|
_buildCollapsedView(context, poll),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
@@ -781,14 +795,19 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
final offset = Tween<Offset>(
|
final offset = Tween<Offset>(
|
||||||
begin: const Offset(0, -0.1),
|
begin: const Offset(0, -0.1),
|
||||||
end: Offset.zero,
|
end: Offset.zero,
|
||||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
).animate(
|
||||||
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
CurvedAnimation(parent: anim, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
final fade = CurvedAnimation(
|
||||||
|
parent: anim,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
return FadeTransition(
|
return FadeTransition(
|
||||||
opacity: fade,
|
opacity: fade,
|
||||||
child: SlideTransition(position: offset, child: child),
|
child: SlideTransition(position: offset, child: child),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: _buildReadonlyView(context),
|
child: _buildReadonlyView(context, poll),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -797,7 +816,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildCollapsedView(context),
|
_buildCollapsedView(context, poll),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
@@ -806,7 +825,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
begin: const Offset(0, -0.1),
|
begin: const Offset(0, -0.1),
|
||||||
end: Offset.zero,
|
end: Offset.zero,
|
||||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
).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(
|
return FadeTransition(
|
||||||
opacity: fade,
|
opacity: fade,
|
||||||
child: SlideTransition(position: offset, child: child),
|
child: SlideTransition(position: offset, child: child),
|
||||||
@@ -816,25 +838,27 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
key: const ValueKey('normal_expanded'),
|
key: const ValueKey('normal_expanded'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(context),
|
_buildHeader(context, poll),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_AnimatedStep(
|
_AnimatedStep(
|
||||||
key: ValueKey(_current.id),
|
key: ValueKey(_current.id),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildBody(context),
|
_buildBody(context, poll),
|
||||||
_buildStats(context, _current),
|
_buildStats(context, _current, poll.stats),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildNavBar(context),
|
_buildNavBar(context, poll),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ class PostComposeCard extends HookConsumerWidget {
|
|||||||
state: composeState,
|
state: composeState,
|
||||||
originalPost: originalPost,
|
originalPost: originalPost,
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
|
useSafeArea: isContained,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
final ComposeState state;
|
final ComposeState state;
|
||||||
final SnPost? originalPost;
|
final SnPost? originalPost;
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
|
final bool useSafeArea;
|
||||||
|
|
||||||
const ComposeToolbar({
|
const ComposeToolbar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.state,
|
required this.state,
|
||||||
this.originalPost,
|
this.originalPost,
|
||||||
this.isCompact = false,
|
this.isCompact = false,
|
||||||
|
this.useSafeArea = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@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? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) {
|
}) {
|
||||||
return fetch(cursor: null);
|
return fetch(cursor: null);
|
||||||
}
|
}
|
||||||
@@ -36,14 +42,20 @@ class PostListNotifier extends _$PostListNotifier
|
|||||||
final queryParams = {
|
final queryParams = {
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'take': _pageSize,
|
'take': _pageSize,
|
||||||
|
'replies': includeReplies,
|
||||||
|
'orderDesc': orderDesc,
|
||||||
|
if (shuffle) 'shuffle': shuffle,
|
||||||
if (pubName != null) 'pub': pubName,
|
if (pubName != null) 'pub': pubName,
|
||||||
if (realm != null) 'realm': realm,
|
if (realm != null) 'realm': realm,
|
||||||
if (type != null) 'type': type,
|
if (type != null) 'type': type,
|
||||||
if (tags != null) 'tags': tags,
|
if (tags != null) 'tags': tags,
|
||||||
if (categories != null) 'categories': categories,
|
if (categories != null) 'categories': categories,
|
||||||
if (shuffle) 'shuffle': true,
|
|
||||||
if (pinned != null) 'pinned': pinned,
|
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(
|
final response = await client.get(
|
||||||
@@ -82,6 +94,14 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
final List<String>? tags;
|
final List<String>? tags;
|
||||||
final bool shuffle;
|
final bool shuffle;
|
||||||
final bool? pinned;
|
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 PostItemType itemType;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
@@ -99,6 +119,13 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
this.tags,
|
this.tags,
|
||||||
this.shuffle = false,
|
this.shuffle = false,
|
||||||
this.pinned,
|
this.pinned,
|
||||||
|
this.includeReplies,
|
||||||
|
this.mediaOnly,
|
||||||
|
this.queryTerm,
|
||||||
|
this.order,
|
||||||
|
this.orderDesc = true,
|
||||||
|
this.periodStart,
|
||||||
|
this.periodEnd,
|
||||||
this.itemType = PostItemType.regular,
|
this.itemType = PostItemType.regular,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.padding,
|
this.padding,
|
||||||
@@ -118,6 +145,13 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc ?? true,
|
||||||
);
|
);
|
||||||
return PagingHelperSliverView(
|
return PagingHelperSliverView(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'post_list.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$postListNotifierHash() => r'fc139ad4df0deb67bcbb949560319f2f7fbfb503';
|
String _$postListNotifierHash() => r'8241120dc3c2004387c6cf881e5cb9224cbd3a97';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -39,6 +39,12 @@ abstract class _$PostListNotifier
|
|||||||
late final bool? pinned;
|
late final bool? pinned;
|
||||||
late final bool shuffle;
|
late final bool shuffle;
|
||||||
late final bool? includeReplies;
|
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({
|
FutureOr<CursorPagingData<SnPost>> build({
|
||||||
String? pubName,
|
String? pubName,
|
||||||
@@ -49,6 +55,12 @@ abstract class _$PostListNotifier
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +84,12 @@ class PostListNotifierFamily
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) {
|
}) {
|
||||||
return PostListNotifierProvider(
|
return PostListNotifierProvider(
|
||||||
pubName: pubName,
|
pubName: pubName,
|
||||||
@@ -82,6 +100,12 @@ class PostListNotifierFamily
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +122,12 @@ class PostListNotifierFamily
|
|||||||
pinned: provider.pinned,
|
pinned: provider.pinned,
|
||||||
shuffle: provider.shuffle,
|
shuffle: provider.shuffle,
|
||||||
includeReplies: provider.includeReplies,
|
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? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) : this._internal(
|
}) : this._internal(
|
||||||
() =>
|
() =>
|
||||||
PostListNotifier()
|
PostListNotifier()
|
||||||
@@ -143,7 +179,13 @@ class PostListNotifierProvider
|
|||||||
..tags = tags
|
..tags = tags
|
||||||
..pinned = pinned
|
..pinned = pinned
|
||||||
..shuffle = shuffle
|
..shuffle = shuffle
|
||||||
..includeReplies = includeReplies,
|
..includeReplies = includeReplies
|
||||||
|
..mediaOnly = mediaOnly
|
||||||
|
..queryTerm = queryTerm
|
||||||
|
..order = order
|
||||||
|
..periodStart = periodStart
|
||||||
|
..periodEnd = periodEnd
|
||||||
|
..orderDesc = orderDesc,
|
||||||
from: postListNotifierProvider,
|
from: postListNotifierProvider,
|
||||||
name: r'postListNotifierProvider',
|
name: r'postListNotifierProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
@@ -161,6 +203,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
|
|
||||||
PostListNotifierProvider._internal(
|
PostListNotifierProvider._internal(
|
||||||
@@ -178,6 +226,12 @@ class PostListNotifierProvider
|
|||||||
required this.pinned,
|
required this.pinned,
|
||||||
required this.shuffle,
|
required this.shuffle,
|
||||||
required this.includeReplies,
|
required this.includeReplies,
|
||||||
|
required this.mediaOnly,
|
||||||
|
required this.queryTerm,
|
||||||
|
required this.order,
|
||||||
|
required this.periodStart,
|
||||||
|
required this.periodEnd,
|
||||||
|
required this.orderDesc,
|
||||||
}) : super.internal();
|
}) : super.internal();
|
||||||
|
|
||||||
final String? pubName;
|
final String? pubName;
|
||||||
@@ -188,6 +242,12 @@ class PostListNotifierProvider
|
|||||||
final bool? pinned;
|
final bool? pinned;
|
||||||
final bool shuffle;
|
final bool shuffle;
|
||||||
final bool? includeReplies;
|
final bool? includeReplies;
|
||||||
|
final bool? mediaOnly;
|
||||||
|
final String? queryTerm;
|
||||||
|
final String? order;
|
||||||
|
final int? periodStart;
|
||||||
|
final int? periodEnd;
|
||||||
|
final bool orderDesc;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
||||||
@@ -202,6 +262,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +285,13 @@ class PostListNotifierProvider
|
|||||||
..tags = tags
|
..tags = tags
|
||||||
..pinned = pinned
|
..pinned = pinned
|
||||||
..shuffle = shuffle
|
..shuffle = shuffle
|
||||||
..includeReplies = includeReplies,
|
..includeReplies = includeReplies
|
||||||
|
..mediaOnly = mediaOnly
|
||||||
|
..queryTerm = queryTerm
|
||||||
|
..order = order
|
||||||
|
..periodStart = periodStart
|
||||||
|
..periodEnd = periodEnd
|
||||||
|
..orderDesc = orderDesc,
|
||||||
from: from,
|
from: from,
|
||||||
name: null,
|
name: null,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
@@ -233,6 +305,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -256,7 +334,13 @@ class PostListNotifierProvider
|
|||||||
other.tags == tags &&
|
other.tags == tags &&
|
||||||
other.pinned == pinned &&
|
other.pinned == pinned &&
|
||||||
other.shuffle == shuffle &&
|
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
|
@override
|
||||||
@@ -270,6 +354,12 @@ class PostListNotifierProvider
|
|||||||
hash = _SystemHash.combine(hash, pinned.hashCode);
|
hash = _SystemHash.combine(hash, pinned.hashCode);
|
||||||
hash = _SystemHash.combine(hash, shuffle.hashCode);
|
hash = _SystemHash.combine(hash, shuffle.hashCode);
|
||||||
hash = _SystemHash.combine(hash, includeReplies.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);
|
return _SystemHash.finish(hash);
|
||||||
}
|
}
|
||||||
@@ -302,6 +392,24 @@ mixin PostListNotifierRef
|
|||||||
|
|
||||||
/// The parameter `includeReplies` of this provider.
|
/// The parameter `includeReplies` of this provider.
|
||||||
bool? get includeReplies;
|
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
|
class _PostListNotifierProviderElement
|
||||||
@@ -331,6 +439,18 @@ class _PostListNotifierProviderElement
|
|||||||
@override
|
@override
|
||||||
bool? get includeReplies =>
|
bool? get includeReplies =>
|
||||||
(origin as PostListNotifierProvider).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
|
// ignore_for_file: type=lint
|
||||||
|
|||||||
@@ -542,6 +542,7 @@ class ReactionDetailsPopup extends HookConsumerWidget {
|
|||||||
notifierRefreshable: provider.notifier,
|
notifierRefreshable: provider.notifier,
|
||||||
contentBuilder:
|
contentBuilder:
|
||||||
(data, widgetCount, endItemView) => ListView.builder(
|
(data, widgetCount, endItemView) => ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
itemCount: widgetCount,
|
itemCount: widgetCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == widgetCount - 1) {
|
if (index == widgetCount - 1) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 3.3.0+143
|
version: 3.3.0+144
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
|
|||||||
Reference in New Issue
Block a user