Compare commits

...

23 Commits

Author SHA1 Message Date
LittleSheep
8e0c0c6054 🚀 Launch 3.1.0+122 2025-08-10 04:16:58 +08:00
LittleSheep
f3d1183076 🐛 Fix android update 2025-08-10 04:16:53 +08:00
LittleSheep
a9f7f0cce0 🐛 Fix bugs, ah bugs ha ha, bugs 2025-08-10 04:04:31 +08:00
LittleSheep
f2943f8411 🐛 Fix iOS notify delegate wrong path 2025-08-10 03:30:57 +08:00
LittleSheep
808e7dcffa Option to boost github assets download 2025-08-10 03:25:57 +08:00
LittleSheep
9bed4fa6fb Android install update 2025-08-10 03:21:34 +08:00
LittleSheep
e6255a340b 🐛 Fix background image didn't apply in certain page 2025-08-10 02:59:28 +08:00
LittleSheep
78bf319fb7 💄 Adjust stickers styles 2025-08-10 02:59:11 +08:00
LittleSheep
36a966d582 🐛 Fix profile link parsing 2025-08-10 02:55:00 +08:00
LittleSheep
f72b268d36 💄 Optimize profile page 2025-08-10 02:29:46 +08:00
LittleSheep
44ef31034e 👽 Update the profile links 2025-08-10 02:17:06 +08:00
LittleSheep
229dc2186f 💄 Optimize markdown rendering 2025-08-10 01:53:14 +08:00
LittleSheep
a2f9a1efb4 Optimize post quick reply 2025-08-10 01:45:02 +08:00
LittleSheep
823e3c5de6 🔀 Merge pull request #160 from Texas0295/v3
[FIX] linux_firebase_guard: skip FirebaseMessaging calls on Linux
2025-08-10 01:11:29 +08:00
Texas0295
faac7bac35 🐛 linux: guard FirebaseMessaging calls when Firebase is not initialized 2025-08-10 00:40:49 +08:00
LittleSheep
1fac1bfe02 🐛 Fix post item and payment 2025-08-10 00:26:32 +08:00
LittleSheep
9394b1d9c8 🐛 Serval bug fixes 2025-08-09 23:24:21 +08:00
LittleSheep
43dd13bac4 🐛 Fix status update issue 2025-08-09 22:55:46 +08:00
LittleSheep
65bc372103 🐛 Fix iOS NSE wrong avatar path 2025-08-09 22:55:35 +08:00
LittleSheep
6558854a7a 🚀 Launch 3.1.0+120 2025-08-09 13:37:35 +08:00
LittleSheep
892035ab27 🐛 Somehow fix production video player issue 2025-08-09 13:35:54 +08:00
LittleSheep
87ae8d2ff4 🚀 Launch 3.1.0+119 2025-08-09 01:44:18 +08:00
LittleSheep
15c2dbaa0d 🐛 Fix developer 2025-08-09 01:41:51 +08:00
37 changed files with 1237 additions and 408 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -706,6 +706,7 @@
"copyToClipboardTooltip": "Copy to clipboard",
"postForwardingTo": "Forwarding to",
"postReplyingTo": "Replying to",
"postReplyPlaceholder": "Post your reply",
"postEditing": "You are editing an existing post",
"postArticle": "Article",
"aboutDeviceName": "Device Name",
@@ -787,5 +788,6 @@
"addLink": "Add link",
"linkKey": "Link Name",
"linkValue": "URL",
"debugOptions": "Debug Options"
"debugOptions": "Debug Options",
"joinedAt": "Joined at {}"
}

View File

@@ -73,6 +73,8 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
@@ -178,25 +180,25 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.50.3):
- sqlite3/common (= 3.50.3)
- sqlite3/common (3.50.3)
- sqlite3/dbstatvtab (3.50.3):
- sqlite3 (3.50.4):
- sqlite3/common (= 3.50.4)
- sqlite3/common (3.50.4)
- sqlite3/dbstatvtab (3.50.4):
- sqlite3/common
- sqlite3/fts5 (3.50.3):
- sqlite3/fts5 (3.50.4):
- sqlite3/common
- sqlite3/math (3.50.3):
- sqlite3/math (3.50.4):
- sqlite3/common
- sqlite3/perf-threadsafe (3.50.3):
- sqlite3/perf-threadsafe (3.50.4):
- sqlite3/common
- sqlite3/rtree (3.50.3):
- sqlite3/rtree (3.50.4):
- sqlite3/common
- sqlite3/session (3.50.3):
- sqlite3/session (3.50.4):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.50.3)
- sqlite3 (~> 3.50.4)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
@@ -223,6 +225,7 @@ DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
@@ -293,6 +296,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility:
@@ -372,6 +377,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
@@ -406,8 +412,8 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d

View File

@@ -34,7 +34,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
}
let serverUrl = UserDefaults.standard.getServerUrl()
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
let url = "\(serverUrl)/sphere/chat/\(metadata["room_id"] ?? "")/messages"
let parameters: [String: Any?] = [
"content": textResponse.userText,

View File

@@ -8,7 +8,7 @@
import Foundation
func getAttachmentUrl(for identifier: String) -> String {
let serverBaseUrl = "https://nt.solian.app"
let serverBaseUrl = "https://api.solian.app"
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/files/\(identifier)"
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
}

View File

@@ -30,7 +30,6 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface.
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
import 'package:island/services/update_service.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
@@ -144,15 +143,6 @@ void main() async {
),
),
);
// Schedule update check shortly after startup, when a context is available.
// Uses the global overlay key to obtain a BuildContext safely.
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = globalOverlay.currentContext;
if (ctx != null) {
UpdateService().checkForUpdates(ctx);
}
});
}
// Router will be provided through Riverpod
@@ -181,6 +171,9 @@ class IslandApp extends HookConsumerWidget {
}
useEffect(() {
if (!kIsWeb && Platform.isLinux) {
return null;
}
const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async {

View File

@@ -1,13 +1,25 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/publisher.dart';
part 'developer.freezed.dart';
part 'developer.g.dart';
@freezed
sealed class SnDeveloper with _$SnDeveloper {
const factory SnDeveloper({
required String id,
required String publisherId,
SnPublisher? publisher,
}) = _SnDeveloper;
factory SnDeveloper.fromJson(Map<String, dynamic> json) =>
_$SnDeveloperFromJson(json);
}
@freezed
sealed class DeveloperStats with _$DeveloperStats {
const factory DeveloperStats({
@Default(0) int totalCustomApps,
}) = _DeveloperStats;
const factory DeveloperStats({@Default(0) int totalCustomApps}) =
_DeveloperStats;
factory DeveloperStats.fromJson(Map<String, dynamic> json) =>
_$DeveloperStatsFromJson(json);

View File

@@ -12,6 +12,293 @@ part of 'developer.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnDeveloper {
String get id; String get publisherId; SnPublisher? get publisher;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnDeveloperCopyWith<SnDeveloper> get copyWith => _$SnDeveloperCopyWithImpl<SnDeveloper>(this as SnDeveloper, _$identity);
/// Serializes this SnDeveloper to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,publisherId,publisher);
@override
String toString() {
return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)';
}
}
/// @nodoc
abstract mixin class $SnDeveloperCopyWith<$Res> {
factory $SnDeveloperCopyWith(SnDeveloper value, $Res Function(SnDeveloper) _then) = _$SnDeveloperCopyWithImpl;
@useResult
$Res call({
String id, String publisherId, SnPublisher? publisher
});
$SnPublisherCopyWith<$Res>? get publisher;
}
/// @nodoc
class _$SnDeveloperCopyWithImpl<$Res>
implements $SnDeveloperCopyWith<$Res> {
_$SnDeveloperCopyWithImpl(this._self, this._then);
final SnDeveloper _self;
final $Res Function(SnDeveloper) _then;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher?,
));
}
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res>? get publisher {
if (_self.publisher == null) {
return null;
}
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
return _then(_self.copyWith(publisher: value));
});
}
}
/// Adds pattern-matching-related methods to [SnDeveloper].
extension SnDeveloperPatterns on SnDeveloper {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnDeveloper value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnDeveloper value) $default,){
final _that = this;
switch (_that) {
case _SnDeveloper():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnDeveloper value)? $default,){
final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String publisherId, SnPublisher? publisher)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that.id,_that.publisherId,_that.publisher);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String publisherId, SnPublisher? publisher) $default,) {final _that = this;
switch (_that) {
case _SnDeveloper():
return $default(_that.id,_that.publisherId,_that.publisher);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String publisherId, SnPublisher? publisher)? $default,) {final _that = this;
switch (_that) {
case _SnDeveloper() when $default != null:
return $default(_that.id,_that.publisherId,_that.publisher);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnDeveloper implements SnDeveloper {
const _SnDeveloper({required this.id, required this.publisherId, this.publisher});
factory _SnDeveloper.fromJson(Map<String, dynamic> json) => _$SnDeveloperFromJson(json);
@override final String id;
@override final String publisherId;
@override final SnPublisher? publisher;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnDeveloperCopyWith<_SnDeveloper> get copyWith => __$SnDeveloperCopyWithImpl<_SnDeveloper>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnDeveloperToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,publisherId,publisher);
@override
String toString() {
return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)';
}
}
/// @nodoc
abstract mixin class _$SnDeveloperCopyWith<$Res> implements $SnDeveloperCopyWith<$Res> {
factory _$SnDeveloperCopyWith(_SnDeveloper value, $Res Function(_SnDeveloper) _then) = __$SnDeveloperCopyWithImpl;
@override @useResult
$Res call({
String id, String publisherId, SnPublisher? publisher
});
@override $SnPublisherCopyWith<$Res>? get publisher;
}
/// @nodoc
class __$SnDeveloperCopyWithImpl<$Res>
implements _$SnDeveloperCopyWith<$Res> {
__$SnDeveloperCopyWithImpl(this._self, this._then);
final _SnDeveloper _self;
final $Res Function(_SnDeveloper) _then;
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) {
return _then(_SnDeveloper(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher?,
));
}
/// Create a copy of SnDeveloper
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res>? get publisher {
if (_self.publisher == null) {
return null;
}
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
return _then(_self.copyWith(publisher: value));
});
}
}
/// @nodoc
mixin _$DeveloperStats {

View File

@@ -6,6 +6,22 @@ part of 'developer.dart';
// JsonSerializableGenerator
// **************************************************************************
_SnDeveloper _$SnDeveloperFromJson(Map<String, dynamic> json) => _SnDeveloper(
id: json['id'] as String,
publisherId: json['publisher_id'] as String,
publisher:
json['publisher'] == null
? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
);
Map<String, dynamic> _$SnDeveloperToJson(_SnDeveloper instance) =>
<String, dynamic>{
'id': instance.id,
'publisher_id': instance.publisherId,
'publisher': instance.publisher?.toJson(),
};
_DeveloperStats _$DeveloperStatsFromJson(Map<String, dynamic> json) =>
_DeveloperStats(
totalCustomApps: (json['total_custom_apps'] as num?)?.toInt() ?? 0,

View File

@@ -25,6 +25,32 @@ sealed class SnAccount with _$SnAccount {
_$SnAccountFromJson(json);
}
@freezed
sealed class ProfileLink with _$ProfileLink {
const factory ProfileLink({required String name, required String url}) =
_ProfileLink;
factory ProfileLink.fromJson(Map<String, dynamic> json) =>
_$ProfileLinkFromJson(json);
}
class ProfileLinkConverter
implements JsonConverter<List<ProfileLink>, dynamic> {
const ProfileLinkConverter();
@override
List<ProfileLink> fromJson(dynamic json) {
return json is List<dynamic>
? json.map((e) => ProfileLink.fromJson(e)).cast<ProfileLink>().toList()
: <ProfileLink>[];
}
@override
List<dynamic> toJson(List<ProfileLink> object) {
return object.map((e) => e.toJson()).toList();
}
}
@freezed
sealed class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({
@@ -38,7 +64,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
@Default('') String location,
@Default('') String timeZone,
DateTime? birthday,
@Default({}) Map<String, String> links,
@ProfileLinkConverter() @Default([]) List<ProfileLink> links,
DateTime? lastSeenAt,
SnAccountBadge? activeBadge,
required int experience,

View File

@@ -347,10 +347,270 @@ $SnWalletSubscriptionRefCopyWith<$Res>? get perkSubscription {
}
/// @nodoc
mixin _$ProfileLink {
String get name; String get url;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ProfileLinkCopyWith<ProfileLink> get copyWith => _$ProfileLinkCopyWithImpl<ProfileLink>(this as ProfileLink, _$identity);
/// Serializes this ProfileLink to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,url);
@override
String toString() {
return 'ProfileLink(name: $name, url: $url)';
}
}
/// @nodoc
abstract mixin class $ProfileLinkCopyWith<$Res> {
factory $ProfileLinkCopyWith(ProfileLink value, $Res Function(ProfileLink) _then) = _$ProfileLinkCopyWithImpl;
@useResult
$Res call({
String name, String url
});
}
/// @nodoc
class _$ProfileLinkCopyWithImpl<$Res>
implements $ProfileLinkCopyWith<$Res> {
_$ProfileLinkCopyWithImpl(this._self, this._then);
final ProfileLink _self;
final $Res Function(ProfileLink) _then;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? url = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [ProfileLink].
extension ProfileLinkPatterns on ProfileLink {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ProfileLink value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ProfileLink value) $default,){
final _that = this;
switch (_that) {
case _ProfileLink():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ProfileLink value)? $default,){
final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String url)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that.name,_that.url);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String url) $default,) {final _that = this;
switch (_that) {
case _ProfileLink():
return $default(_that.name,_that.url);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String url)? $default,) {final _that = this;
switch (_that) {
case _ProfileLink() when $default != null:
return $default(_that.name,_that.url);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ProfileLink implements ProfileLink {
const _ProfileLink({required this.name, required this.url});
factory _ProfileLink.fromJson(Map<String, dynamic> json) => _$ProfileLinkFromJson(json);
@override final String name;
@override final String url;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ProfileLinkCopyWith<_ProfileLink> get copyWith => __$ProfileLinkCopyWithImpl<_ProfileLink>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ProfileLinkToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,url);
@override
String toString() {
return 'ProfileLink(name: $name, url: $url)';
}
}
/// @nodoc
abstract mixin class _$ProfileLinkCopyWith<$Res> implements $ProfileLinkCopyWith<$Res> {
factory _$ProfileLinkCopyWith(_ProfileLink value, $Res Function(_ProfileLink) _then) = __$ProfileLinkCopyWithImpl;
@override @useResult
$Res call({
String name, String url
});
}
/// @nodoc
class __$ProfileLinkCopyWithImpl<$Res>
implements _$ProfileLinkCopyWith<$Res> {
__$ProfileLinkCopyWithImpl(this._self, this._then);
final _ProfileLink _self;
final $Res Function(_ProfileLink) _then;
/// Create a copy of ProfileLink
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? url = null,}) {
return _then(_ProfileLink(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
mixin _$SnAccountProfile {
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; Map<String, String> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -383,7 +643,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult
$Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -413,7 +673,7 @@ as String,location: null == location ? _self.location : location // ignore: cast
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,links: null == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
@@ -554,7 +814,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
@@ -575,7 +835,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile():
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
@@ -592,7 +852,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
@@ -607,7 +867,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
@JsonSerializable()
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, final Map<String, String> links = const {}, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id;
@@ -620,11 +880,11 @@ class _SnAccountProfile implements SnAccountProfile {
@override@JsonKey() final String location;
@override@JsonKey() final String timeZone;
@override final DateTime? birthday;
final Map<String, String> _links;
@override@JsonKey() Map<String, String> get links {
if (_links is EqualUnmodifiableMapView) return _links;
final List<ProfileLink> _links;
@override@JsonKey()@ProfileLinkConverter() List<ProfileLink> get links {
if (_links is EqualUnmodifiableListView) return _links;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_links);
return EqualUnmodifiableListView(_links);
}
@override final DateTime? lastSeenAt;
@@ -672,7 +932,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult
$Res call({
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -702,7 +962,7 @@ as String,location: null == location ? _self.location : location // ignore: cast
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,links: null == links ? _self._links : links // ignore: cast_nullable_to_non_nullable
as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable

View File

@@ -47,6 +47,12 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_ProfileLink _$ProfileLinkFromJson(Map<String, dynamic> json) =>
_ProfileLink(name: json['name'] as String, url: json['url'] as String);
Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) =>
<String, dynamic>{'name': instance.name, 'url': instance.url};
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile(
id: json['id'] as String,
@@ -63,10 +69,9 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
? null
: DateTime.parse(json['birthday'] as String),
links:
(json['links'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
) ??
const {},
json['links'] == null
? const []
: const ProfileLinkConverter().fromJson(json['links']),
lastSeenAt:
json['last_seen_at'] == null
? null
@@ -116,7 +121,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'location': instance.location,
'time_zone': instance.timeZone,
'birthday': instance.birthday?.toIso8601String(),
'links': instance.links,
'links': const ProfileLinkConverter().toJson(instance.links),
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'active_badge': instance.activeBadge?.toJson(),
'experience': instance.experience,

View File

@@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/udid.native.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/services/update_service.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -205,33 +205,16 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
// Fetch latest release and show the unified sheet
final svc = UpdateService();
// Reuse service fetch + compare to decide content
showLoadingModal(context);
final release = await svc.fetchLatestRelease();
if (!context.mounted) return;
hideLoadingModal(context);
if (release != null) {
await svc.showUpdateSheet(context, release);
} else {
// Fallback: show a simple sheet indicating no info
// Use your SheetScaffold for consistent styling
// Show a minimal message
// ignore: use_build_context_synchronously
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
showDragHandle: true,
backgroundColor:
Theme.of(context).colorScheme.surface,
builder:
(_) => const SheetScaffold(
titleText: 'Update',
child: Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Unable to fetch release info at this time.',
),
),
),
),
showInfoAlert(
'Currently cannot get update from the GitHub.',
'Unable to check for updates',
);
}
},

View File

@@ -7,6 +7,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
@@ -95,11 +96,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
final usernameController = useTextEditingController(text: user.value!.name);
final nicknameController = useTextEditingController(text: user.value!.nick);
final language = useState(user.value!.language);
final links = useState<List<Map<String, String>>>(
user.value!.profile.links.entries
.map((e) => {'key': e.key, 'value': e.value})
.toList(),
);
final links = useState<List<ProfileLink>>(user.value!.profile.links);
void updateBasicInfo() async {
if (!formKeyBasicInfo.currentState!.validate()) return;
@@ -171,7 +168,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
'location': locationController.text,
'time_zone': timeZoneController.text,
'birthday': birthday.value?.toUtc().toIso8601String(),
'links': {for (var e in links.value) e['key']!: e['value']!},
'links': links.value,
},
);
final userNotifier = ref.read(userInfoProvider.notifier);
@@ -575,13 +572,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
children: [
Expanded(
child: TextFormField(
initialValue: links.value[i]['key'],
initialValue: links.value[i].name,
decoration: InputDecoration(
labelText: 'linkKey'.tr(),
isDense: true,
),
onChanged: (value) {
links.value[i]['key'] = value;
links.value[i] = links.value[i].copyWith(
name: value,
);
},
onTapOutside:
(_) =>
@@ -592,13 +591,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
const Gap(8),
Expanded(
child: TextFormField(
initialValue: links.value[i]['value'],
initialValue: links.value[i].url,
decoration: InputDecoration(
labelText: 'linkValue'.tr(),
isDense: true,
),
onChanged: (value) {
links.value[i]['value'] = value;
links.value[i] = links.value[i].copyWith(
url: value,
);
},
onTapOutside:
(_) =>
@@ -620,7 +621,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: FilledButton.icon(
onPressed: () {
links.value = List.from(links.value)
..add({'key': '', 'value': ''});
..add(ProfileLink(name: '', url: ''));
},
label: Text('addLink').tr(),
icon: const Icon(Symbols.add),

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
@@ -196,6 +197,15 @@ class AccountProfileScreen extends HookConsumerWidget {
List<Widget> buildSubcolumn(SnAccount data) {
return [
Row(
spacing: 6,
children: [
const Icon(Symbols.join, size: 17, fill: 1),
Text(
'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]),
),
],
),
if (data.profile.birthday != null)
Row(
spacing: 6,
@@ -322,7 +332,7 @@ class AccountProfileScreen extends HookConsumerWidget {
spacing: 2,
children: buildSubcolumn(data),
),
if (data.profile.timeZone.isNotEmpty)
if (data.profile.timeZone.isNotEmpty && !kIsWeb)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -357,17 +367,21 @@ class AccountProfileScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
for (final link in data.profile.links.entries)
for (final link in data.profile.links)
ListTile(
title: Text(link.key.capitalizeEachWord()),
subtitle: Text(link.value),
title: Text(link.name.capitalizeEachWord()),
subtitle: Text(link.url),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
onTap: () {
launchUrlString(link.value);
if (!link.url.startsWith('http') && !link.url.contains('://')) {
launchUrlString('https://${link.url}');
} else {
launchUrlString(link.url);
}
},
),
],
@@ -561,6 +575,7 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter(
child: accountProfileBio(data).padding(top: 4),
),
if (data.profile.links.isNotEmpty)
SliverToBoxAdapter(
child: accountProfileLinks(data),
),
@@ -660,6 +675,7 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter(
child: accountProfileBio(data).padding(horizontal: 4),
),
if (data.profile.links.isNotEmpty)
SliverToBoxAdapter(
child: accountProfileLinks(
data,

View File

@@ -339,7 +339,7 @@ class ChatRoomScreen extends HookConsumerWidget {
}
await apiClient.post(
'/chat/${chatRoom.value!.id}/members/me',
'/sphere/chat/${chatRoom.value!.id}/members/me',
);
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
@@ -929,7 +929,7 @@ class ChatRoomScreen extends HookConsumerWidget {
if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/files/${attachment.data.id}',
'/drive/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/poll/poll_feedback.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -70,7 +71,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
return AppScaffold(
appBar: AppBar(title: const Text('Polls')),
floatingActionButton: FloatingActionButton(
onPressed: () => _createPoll(context),

View File

@@ -58,7 +58,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
try {
showLoadingModal(context);
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/stickers/$id/content/${sticker.id}');
await apiClient.delete('/sphere/stickers/$id/content/${sticker.id}');
ref.invalidate(stickerPackContentProvider(id));
} catch (err) {
showErrorAlert(err);
@@ -297,7 +297,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
).then((confirm) {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete('/stickers/$packId');
client.delete('/sphere/stickers/$packId');
ref.invalidate(stickerPacksNotifierProvider);
if (context.mounted) context.pop(true);
}
@@ -325,7 +325,7 @@ Future<SnSticker?> stickerPackSticker(
if (query == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get(
'/stickers/${query.packId}/content/${query.id}',
'/sphere/stickers/${query.packId}/content/${query.id}',
);
if (resp.data == null) return null;
return SnSticker.fromJson(resp.data);
@@ -379,8 +379,8 @@ class EditStickersScreen extends HookConsumerWidget {
try {
final resp = await apiClient.request(
id == null
? '/stickers/$packId/content'
: '/stickers/$packId/content/$id',
? '/sphere/stickers/$packId/content'
: '/sphere/stickers/$packId/content/$id',
data: {'slug': slugController.text, 'image_id': imageController.text},
options: Options(method: id == null ? 'POST' : 'PATCH'),
);

View File

@@ -151,7 +151,7 @@ class _StickerPackContentProviderElement
}
String _$stickerPackStickerHash() =>
r'36f524c047e632236d5597aaaa8678ed86599602';
r'5c553666b3a63530bdebae4b7cd52f303c5ab3a0';
/// See also [stickerPackSticker].
@ProviderFor(stickerPackSticker)

View File

@@ -114,10 +114,11 @@ class WebFeedEditScreen extends HookConsumerWidget {
return feedAsync.when(
loading:
() =>
const Scaffold(body: Center(child: CircularProgressIndicator())),
() => const AppScaffold(
body: Center(child: CircularProgressIndicator()),
),
error:
(error, stack) => Scaffold(
(error, stack) => AppScaffold(
appBar: AppBar(title: const Text('Error')),
body: Center(child: Text('Error: $error')),
),

View File

@@ -30,12 +30,12 @@ Future<DeveloperStats?> developerStats(Ref ref, String? uname) async {
}
@riverpod
Future<List<SnPublisher>> developers(Ref ref) async {
Future<List<SnDeveloper>> developers(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers');
return resp.data
.map((e) => SnPublisher.fromJson(e))
.cast<SnPublisher>()
.map((e) => SnDeveloper.fromJson(e))
.cast<SnDeveloper>()
.toList();
}
@@ -74,25 +74,25 @@ class DeveloperHubScreen extends HookConsumerWidget {
}
final developers = ref.watch(developersProvider);
final currentDeveloper = useState<SnPublisher?>(
final currentDeveloper = useState<SnDeveloper?>(
developers.value?.firstOrNull,
);
final List<DropdownMenuItem<SnPublisher>> developersMenu = developers.when(
final List<DropdownMenuItem<SnDeveloper>> developersMenu = developers.when(
data:
(data) =>
data
.map(
(item) => DropdownMenuItem<SnPublisher>(
(item) => DropdownMenuItem<SnDeveloper>(
value: item,
child: ListTile(
minTileHeight: 48,
leading: ProfilePictureWidget(
radius: 16,
fileId: item.picture?.id,
fileId: item.publisher?.picture?.id,
),
title: Text(item.nick),
subtitle: Text('@${item.name}'),
title: Text(item.publisher!.nick),
subtitle: Text('@${item.publisher!.name}'),
trailing:
currentDeveloper.value?.id == item.id
? const Icon(Icons.check)
@@ -107,7 +107,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
);
final developerStats = ref.watch(
developerStatsProvider(currentDeveloper.value?.name),
developerStatsProvider(currentDeveloper.value?.publisher?.name),
);
return AppScaffold(
@@ -117,7 +117,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
title: Text('developerHub').tr(),
actions: [
DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>(
child: DropdownButton2<SnDeveloper>(
alignment: Alignment.centerRight,
value: currentDeveloper.value,
hint: CircleAvatar(
@@ -139,7 +139,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
...developersMenu.map(
(e) => ProfilePictureWidget(
radius: 16,
fileId: e.value?.picture?.id,
fileId: e.value?.publisher?.picture?.id,
).center().padding(right: 8),
),
];
@@ -193,10 +193,12 @@ class DeveloperHubScreen extends HookConsumerWidget {
...(developers.value?.map(
(developer) => ListTile(
leading: ProfilePictureWidget(
file: developer.picture,
file: developer.publisher?.picture,
),
title: Text(developer.publisher!.nick),
subtitle: Text(
'@${developer.publisher!.name}',
),
title: Text(developer.nick),
subtitle: Text('@${developer.name}'),
onTap: () {
currentDeveloper.value = developer;
},
@@ -243,7 +245,8 @@ class DeveloperHubScreen extends HookConsumerWidget {
context.pushNamed(
'developerApps',
pathParameters: {
'name': currentDeveloper.value!.name,
'name':
currentDeveloper.value!.publisher!.name,
},
);
},
@@ -257,7 +260,9 @@ class DeveloperHubScreen extends HookConsumerWidget {
error: err,
onRetry: () {
ref.invalidate(
developerStatsProvider(currentDeveloper.value?.name),
developerStatsProvider(
currentDeveloper.value?.publisher!.name,
),
);
},
),
@@ -354,7 +359,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
? Center(
child:
Text(
'noPublishersToEnroll',
'noDevelopersToEnroll',
textAlign: TextAlign.center,
).tr(),
)

View File

@@ -149,12 +149,12 @@ class _DeveloperStatsProviderElement
String? get uname => (origin as DeveloperStatsProvider).uname;
}
String _$developersHash() => r'04f25db31f511f651a5add128d56631236ed0b39';
String _$developersHash() => r'252341098617ac398ce133994453f318dd3edbd2';
/// See also [developers].
@ProviderFor(developers)
final developersProvider =
AutoDisposeFutureProvider<List<SnPublisher>>.internal(
AutoDisposeFutureProvider<List<SnDeveloper>>.internal(
developers,
name: r'developersProvider',
debugGetCreateSourceHash:
@@ -167,6 +167,6 @@ final developersProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnPublisher>>;
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnDeveloper>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -9,6 +9,7 @@ import 'package:gap/gap.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/models/poll.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:uuid/uuid.dart';
class PollEditorState {
@@ -413,7 +414,7 @@ class PollEditorScreen extends ConsumerWidget {
});
}
return Scaffold(
return AppScaffold(
appBar: AppBar(
title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'),
actions: [
@@ -428,7 +429,9 @@ class PollEditorScreen extends ConsumerWidget {
const Gap(8),
],
),
body: SafeArea(
body: Column(
children: [
Expanded(
child: Form(
key: ValueKey(model.id),
child: ListView(
@@ -512,7 +515,8 @@ class PollEditorScreen extends ConsumerWidget {
if (model.questions.isEmpty)
_EmptyState(
title: 'No questions yet',
subtitle: 'Use "Add question" to start building your poll.',
subtitle:
'Use "Add question" to start building your poll.',
)
else
ReorderableListView.builder(
@@ -559,7 +563,10 @@ class PollEditorScreen extends ConsumerWidget {
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(16),
child: _QuestionEditor(index: index, question: q),
child: _QuestionEditor(
index: index,
question: q,
),
),
],
),
@@ -571,14 +578,7 @@ class PollEditorScreen extends ConsumerWidget {
),
),
),
bottomNavigationBar: Padding(
padding: EdgeInsets.fromLTRB(
16,
8,
16,
16 + MediaQuery.of(context).padding.bottom,
),
child: Row(
Row(
children: [
OutlinedButton.icon(
onPressed: () {
@@ -597,6 +597,7 @@ class PollEditorScreen extends ConsumerWidget {
),
],
),
],
),
);
}

View File

@@ -92,6 +92,7 @@ class PostDetailScreen extends HookConsumerWidget {
right: 0,
child: Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainer,
child: postState
.when(
data:
@@ -107,8 +108,8 @@ class PostDetailScreen extends HookConsumerWidget {
error: (_, _) => const SizedBox.shrink(),
)
.padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,
bottom: MediaQuery.of(context).padding.bottom + 8,
top: 8,
horizontal: 16,
),
),

View File

@@ -1,19 +1,28 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_update/azhon_app_update.dart';
import 'package:flutter_app_update/update_model.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:collection/collection.dart'; // Added for firstWhereOrNull
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:island/widgets/content/sheet.dart';
/// Data model for a GitHub release we care about
class GithubReleaseInfo {
final String tagName; // e.g. 3.1.0+118
final String name; // release title
final String body; // changelog markdown
final String htmlUrl; // release page
final String tagName;
final String name;
final String body;
final String htmlUrl;
final DateTime createdAt;
final List<GithubReleaseAsset> assets;
const GithubReleaseInfo({
required this.tagName,
@@ -21,9 +30,28 @@ class GithubReleaseInfo {
required this.body,
required this.htmlUrl,
required this.createdAt,
this.assets = const [],
});
}
/// Data model for a GitHub release asset
class GithubReleaseAsset {
final String name;
final String browserDownloadUrl;
const GithubReleaseAsset({
required this.name,
required this.browserDownloadUrl,
});
factory GithubReleaseAsset.fromJson(Map<String, dynamic> json) {
return GithubReleaseAsset(
name: json['name'] as String,
browserDownloadUrl: json['browser_download_url'] as String,
);
}
}
/// Parses version and build number from "x.y.z+build"
class _ParsedVersion implements Comparable<_ParsedVersion> {
final int major;
@@ -62,7 +90,7 @@ class _ParsedVersion implements Comparable<_ParsedVersion> {
}
class UpdateService {
UpdateService({Dio? dio})
UpdateService({Dio? dio, this.useProxy = false})
: _dio =
dio ??
Dio(
@@ -78,6 +106,9 @@ class UpdateService {
);
final Dio _dio;
final bool useProxy;
static const _proxyBaseUrl = 'https://ghfast.top/';
static const _releasesLatestApi =
'https://api.github.com/repos/solsynth/solian/releases/latest';
@@ -85,31 +116,52 @@ class UpdateService {
/// Checks GitHub for the latest release and compares against the current app version.
/// If update is available, shows a bottom sheet with changelog and an action to open release page.
Future<void> checkForUpdates(BuildContext context) async {
log('[Update] Checking for updates...');
try {
final release = await fetchLatestRelease();
if (release == null) return;
if (release == null) {
log('[Update] No latest release found or could not fetch.');
return;
}
log('[Update] Fetched latest release: ${release.tagName}');
final info = await PackageInfo.fromPlatform();
final localVersionStr = '${info.version}+${info.buildNumber}';
log('[Update] Local app version: $localVersionStr');
final latest = _ParsedVersion.tryParse(release.tagName);
final local = _ParsedVersion.tryParse(localVersionStr);
if (latest == null || local == null) {
log(
'[Update] Failed to parse versions. Latest: ${release.tagName}, Local: $localVersionStr',
);
// If parsing fails, do nothing silently
return;
}
log('[Update] Parsed versions. Latest: $latest, Local: $local');
final needsUpdate = latest.compareTo(local) > 0;
if (!needsUpdate) return;
if (!needsUpdate) {
log('[Update] App is up to date. No update needed.');
return;
}
log('[Update] Update available! Latest: $latest, Local: $local');
if (!context.mounted) return;
if (!context.mounted) {
log('[Update] Context not mounted, cannot show update sheet.');
return;
}
// Delay to ensure UI is ready (if called at startup)
await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) {
await showUpdateSheet(context, release);
} catch (_) {
log('[Update] Update sheet shown.');
}
} catch (e) {
log('[Update] Error checking for updates: $e');
// Ignore errors (network, api, etc.)
return;
}
@@ -126,8 +178,12 @@ class UpdateService {
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder:
(ctx) => _UpdateSheet(
builder: (ctx) {
String? androidUpdateUrl;
if (Platform.isAndroid) {
androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
}
return _UpdateSheet(
release: release,
onOpen: () async {
final uri = Uri.parse(release.htmlUrl);
@@ -135,16 +191,55 @@ class UpdateService {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
),
androidUpdateUrl: androidUpdateUrl,
useProxy: useProxy, // Pass the useProxy flag
);
},
);
}
String? _getAndroidUpdateUrl(List<GithubReleaseAsset> assets) {
final arm64 = assets.firstWhereOrNull(
(asset) => asset.name == 'app-arm64-v8a-release.apk',
);
final armeabi = assets.firstWhereOrNull(
(asset) => asset.name == 'app-armeabi-v7a-release.apk',
);
final x86_64 = assets.firstWhereOrNull(
(asset) => asset.name == 'app-x86_64-release.apk',
);
// Prioritize arm64, then armeabi, then x86_64
if (arm64 != null) {
return arm64.browserDownloadUrl;
} else if (armeabi != null) {
return armeabi.browserDownloadUrl;
} else if (x86_64 != null) {
return x86_64.browserDownloadUrl;
}
return null;
}
/// Fetch the latest release info from GitHub.
/// Public so other screens (e.g., About) can manually trigger update checks.
Future<GithubReleaseInfo?> fetchLatestRelease() async {
final resp = await _dio.get(_releasesLatestApi);
if (resp.statusCode != 200) return null;
final apiEndpoint =
useProxy
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
: _releasesLatestApi;
log(
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
);
final resp = await _dio.get(apiEndpoint);
if (resp.statusCode != 200) {
log(
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
);
return null;
}
final data = resp.data as Map<String, dynamic>;
log('[Update] Successfully fetched release data.');
final tagName = (data['tag_name'] ?? '').toString();
final name = (data['name'] ?? tagName).toString();
@@ -152,25 +247,70 @@ class UpdateService {
final htmlUrl = (data['html_url'] ?? '').toString();
final createdAtStr = (data['created_at'] ?? '').toString();
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
final assetsData =
(data['assets'] as List<dynamic>?)
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
if (tagName.isEmpty || htmlUrl.isEmpty) return null;
if (tagName.isEmpty || htmlUrl.isEmpty) {
log(
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
);
return null;
}
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
return GithubReleaseInfo(
tagName: tagName,
name: name,
body: body,
htmlUrl: htmlUrl,
createdAt: createdAt,
assets: assetsData,
);
}
}
class _UpdateSheet extends StatelessWidget {
const _UpdateSheet({required this.release, required this.onOpen});
class _UpdateSheet extends StatefulWidget {
const _UpdateSheet({
required this.release,
required this.onOpen,
this.androidUpdateUrl,
this.useProxy = false,
});
final String? androidUpdateUrl;
final bool useProxy;
final GithubReleaseInfo release;
final VoidCallback onOpen;
@override
State<_UpdateSheet> createState() => _UpdateSheetState();
}
class _UpdateSheetState extends State<_UpdateSheet> {
late bool _useProxy;
@override
void initState() {
super.initState();
_useProxy = widget.useProxy;
}
Future<void> _installUpdate(String url) async {
final downloadUrl =
_useProxy ? 'https://ghfast.top/${Uri.encodeComponent(url)}' : url;
UpdateModel model = UpdateModel(
downloadUrl,
"solian-update-${widget.release.tagName}.apk",
"launcher_icon",
'https://apps.apple.com/us/app/solian/id6499032345',
);
AzhonAppUpdate.update(model);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@@ -186,8 +326,11 @@ class _UpdateSheet extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(release.name, style: theme.textTheme.titleMedium).bold(),
Text(release.tagName).fontSize(12),
Text(
widget.release.name,
style: theme.textTheme.titleMedium,
).bold(),
Text(widget.release.tagName).fontSize(12),
],
).padding(vertical: 16, horizontal: 16),
const Divider(height: 1),
@@ -197,21 +340,45 @@ class _UpdateSheet extends StatelessWidget {
horizontal: 16,
vertical: 16,
),
child: SelectableText(
release.body.isEmpty
child: MarkdownTextContent(
content:
widget.release.body.isEmpty
? 'No changelog provided.'
: release.body,
style: theme.textTheme.bodyMedium,
: widget.release.body,
),
),
),
if (!kIsWeb && Platform.isAndroid)
SwitchListTile(
title: const Text('Use GitHub Proxy for Download'),
value: _useProxy,
onChanged: (value) {
setState(() {
_useProxy = value;
});
},
).padding(horizontal: 8),
Column(
children: [
Row(
spacing: 8,
children: [
if (!kIsWeb &&
Platform.isAndroid &&
widget.androidUpdateUrl != null)
Expanded(
child: FilledButton.icon(
onPressed: onOpen,
onPressed: () {
log(widget.androidUpdateUrl!);
_installUpdate(widget.androidUpdateUrl!);
},
icon: const Icon(Symbols.update),
label: const Text('Install update'),
),
),
Expanded(
child: FilledButton.icon(
onPressed: widget.onOpen,
icon: const Icon(Icons.open_in_new),
label: const Text('Open release page'),
),

View File

@@ -55,7 +55,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
'attitude': attitude.value,
'is_invisible': isInvisible.value,
'is_not_disturb': isNotDisturb.value,
'cleared_at': clearedAt.value?.toIso8601String(),
'cleared_at': clearedAt.value?.toUtc().toIso8601String(),
if (labelController.text.isNotEmpty) 'label': labelController.text,
},
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),

View File

@@ -331,7 +331,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
final user = ref.watch(userInfoProvider);
final websocketState = ref.watch(websocketStateProvider);
final indicatorHeight =
MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 20);
MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 25);
Color indicatorColor;
String indicatorText;

View File

@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/sharing_intent.dart';
import 'package:island/services/update_service.dart';
import 'package:island/widgets/content/network_status_sheet.dart';
import 'package:island/widgets/tour/tour.dart';
@@ -21,6 +22,7 @@ class AppWrapper extends HookConsumerWidget {
});
final sharingService = SharingIntentService();
sharingService.initialize(context);
UpdateService().checkForUpdates(context);
return () {
sharingService.dispose();
ntySubs?.cancel();

View File

@@ -142,7 +142,7 @@ class CloudVideoWidget extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
Wrap(
spacing: 8,
children: [
if (item.fileMeta?['duration'] != null)
@@ -199,8 +199,8 @@ class CloudVideoWidget extends HookConsumerWidget {
),
),
],
),
).padding(horizontal: 16, bottom: 12),
),
],
),
onTap: () {

View File

@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_highlight/themes/a11y-dark.dart';
import 'package:flutter_highlight/themes/a11y-light.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
@@ -71,7 +72,22 @@ class MarkdownTextContent extends HookConsumerWidget {
textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!,
),
HrConfig(height: 1, color: Theme.of(context).dividerColor),
PreConfig(theme: isDark ? a11yDarkTheme : a11yLightTheme),
PreConfig(
theme: isDark ? a11yDarkTheme : a11yLightTheme,
textStyle: GoogleFonts.robotoMono(fontSize: 14),
styleNotMatched: GoogleFonts.robotoMono(fontSize: 14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
),
TableConfig(
wrapper:
(child) => SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: child,
),
),
LinkConfig(
style:
linkStyle ??
@@ -160,7 +176,7 @@ class MarkdownTextContent extends HookConsumerWidget {
uri: stickerUri,
width: size,
height: size,
fit: BoxFit.cover,
fit: BoxFit.contain,
noCacheOptimization: true,
),
),

View File

@@ -248,7 +248,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
try {
final client = ref.read(apiClientProvider);
final response = await client.post(
'/orders/${widget.order.id}/pay',
'/id/orders/${widget.order.id}/pay',
data: {'pin_code': pin},
);

View File

@@ -273,7 +273,7 @@ class PostItem extends HookConsumerWidget {
: item.reactionsCount.entries
.sortedBy((e) => e.value)
.map((e) => e.key)
.first;
.last;
final postLanguage =
item.content != null
@@ -480,7 +480,9 @@ class PostItem extends HookConsumerWidget {
],
),
)
else if (item.content?.isNotEmpty ?? false)
else if ((item.content?.isNotEmpty ?? false) ||
(item.title?.isNotEmpty ?? false) ||
(item.description?.isNotEmpty ?? false))
Padding(
padding: EdgeInsets.only(
left: renderingPadding.horizontal,

View File

@@ -1,11 +1,13 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.dart';
@@ -14,8 +16,14 @@ import 'package:styled_widget/styled_widget.dart';
class PostQuickReply extends HookConsumerWidget {
final SnPost parent;
final Function? onPosted;
const PostQuickReply({super.key, required this.parent, this.onPosted});
final VoidCallback? onPosted;
final VoidCallback? onLaunch;
const PostQuickReply({
super.key,
required this.parent,
this.onPosted,
this.onLaunch,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -48,7 +56,7 @@ class PostQuickReply extends HookConsumerWidget {
'content': contentController.text,
'replied_post_id': parent.id,
},
options: Options(headers: {'X-Pub': currentPublisher.value?.name}),
queryParameters: {'pub': currentPublisher.value?.name},
);
contentController.clear();
onPosted?.call();
@@ -83,9 +91,10 @@ class PostQuickReply extends HookConsumerWidget {
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: 'Post your reply',
border: const OutlineInputBorder(),
hintText: 'postReplyPlaceholder'.tr(),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
@@ -97,6 +106,26 @@ class PostQuickReply extends HookConsumerWidget {
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
IconButton(
onPressed: () {
onLaunch?.call();
GoRouter.of(context)
.pushNamed(
'postCompose',
extra: PostComposeInitialState(
content: contentController.text,
replyingTo: parent,
),
)
.then((value) {
if (value != null) onPosted?.call();
});
},
icon: const Icon(Symbols.launch, size: 20),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
@@ -110,6 +139,7 @@ class PostQuickReply extends HookConsumerWidget {
: Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction,
constraints: const BoxConstraints(),
),
],
),

View File

@@ -38,14 +38,18 @@ class PostRepliesSheet extends HookConsumerWidget {
if (user.value != null)
Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: PostQuickReply(
parent: post,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(post.id));
},
onLaunch: () {
Navigator.of(context).pop();
},
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,
bottom: MediaQuery.of(context).padding.bottom + 8,
top: 8,
horizontal: 16,
),
),

View File

@@ -130,25 +130,25 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.50.3):
- sqlite3/common (= 3.50.3)
- sqlite3/common (3.50.3)
- sqlite3/dbstatvtab (3.50.3):
- sqlite3 (3.50.4):
- sqlite3/common (= 3.50.4)
- sqlite3/common (3.50.4)
- sqlite3/dbstatvtab (3.50.4):
- sqlite3/common
- sqlite3/fts5 (3.50.3):
- sqlite3/fts5 (3.50.4):
- sqlite3/common
- sqlite3/math (3.50.3):
- sqlite3/math (3.50.4):
- sqlite3/common
- sqlite3/perf-threadsafe (3.50.3):
- sqlite3/perf-threadsafe (3.50.4):
- sqlite3/common
- sqlite3/rtree (3.50.3):
- sqlite3/rtree (3.50.4):
- sqlite3/common
- sqlite3/session (3.50.3):
- sqlite3/session (3.50.4):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.50.3)
- sqlite3 (~> 3.50.4)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
@@ -328,8 +328,8 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd

View File

@@ -73,22 +73,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
auto_route:
dependency: transitive
description:
name: auto_route
sha256: b8c036fa613a98a759cf0fdcba26e62f4985dcbff01a5e760ab411e8554bbaf0
url: "https://pub.dev"
source: hosted
version: "10.1.0+1"
auto_route_generator:
dependency: "direct dev"
description:
name: auto_route_generator
sha256: "9e3846fcbeacba5c362557328dd8c8fbc953b6a0cbc3395365e8d8f92eea29c4"
url: "https://pub.dev"
source: hosted
version: "10.1.0"
avatar_stack:
dependency: "direct main"
description:
@@ -205,10 +189,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62"
sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb
url: "https://pub.dev"
source: hosted
version: "8.11.0"
version: "8.11.1"
cached_network_image:
dependency: "direct main"
description:
@@ -453,10 +437,10 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.dev"
source: hosted
version: "5.8.0+1"
version: "5.9.0"
dio_web_adapter:
dependency: transitive
description:
@@ -573,10 +557,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "13ba4e627ef24503a465d1d61b32596ce10eb6b8903678d362a528f9939b4aa8"
sha256: "8f9f429998f9232d65bc4757af74475ce44fc80f10704ff5dfa8b1d14fc429b9"
url: "https://pub.dev"
source: hosted
version: "10.2.1"
version: "10.2.3"
file_selector_linux:
dependency: transitive
description:
@@ -678,6 +662,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_app_update:
dependency: "direct main"
description:
name: flutter_app_update
sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
flutter_blurhash:
dependency: "direct main"
description:
@@ -911,10 +903,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
url: "https://pub.dev"
source: hosted
version: "2.0.28"
version: "2.0.29"
flutter_popup_card:
dependency: "direct main"
description:
@@ -1033,10 +1025,10 @@ packages:
dependency: transitive
description:
name: font_awesome_flutter
sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a
sha256: f50ce90dbe26d977415b9540400d6778bef00894aced6358ae578abd92b14b10
url: "https://pub.dev"
source: hosted
version: "10.8.0"
version: "10.9.0"
freezed:
dependency: "direct dev"
description:
@@ -1089,10 +1081,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431
sha256: "8b1f37dfaf6e958c6b872322db06f946509433bec3de753c3491a42ae9ec2b48"
url: "https://pub.dev"
source: hosted
version: "16.0.0"
version: "16.1.0"
google_fonts:
dependency: "direct main"
description:
@@ -1153,10 +1145,10 @@ packages:
dependency: transitive
description:
name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.5.0"
http_multi_server:
dependency: transitive
description:
@@ -1193,10 +1185,10 @@ packages:
dependency: "direct main"
description:
name: image_picker_android
sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d"
sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6
url: "https://pub.dev"
source: hosted
version: "0.8.12+24"
version: "0.8.12+25"
image_picker_for_web:
dependency: transitive
description:
@@ -1361,18 +1353,18 @@ packages:
dependency: transitive
description:
name: local_auth_android
sha256: "82b2bdeee2199a510d3b7716121e96a6609da86693bb0863edd8566355406b79"
sha256: "316503f6772dea9c0c038bb7aac4f68ab00112d707d258c770f7fc3c250a2d88"
url: "https://pub.dev"
source: hosted
version: "1.0.50"
version: "1.0.51"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "25163ce60a5a6c468cf7a0e3dc8a165f824cabc2aa9e39a5e9fc5c2311b7686f"
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "1.6.0"
local_auth_platform_interface:
dependency: transitive
description:
@@ -2033,10 +2025,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
url: "https://pub.dev"
source: hosted
version: "2.4.10"
version: "2.4.11"
shared_preferences_foundation:
dependency: transitive
description:
@@ -2206,18 +2198,18 @@ packages:
dependency: transitive
description:
name: sqlite3
sha256: dd806fff004a0aeb01e208b858dbc649bc72104670d425a81a6dd17698535f6e
sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924
url: "https://pub.dev"
source: hosted
version: "2.8.0"
version: "2.9.0"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: fd996da5515a73aacd0a04ae7063db5fe8df42670d974df4c3ee538c652eef2e
sha256: "2b03273e71867a8a4d030861fc21706200debe5c5858a4b9e58f4a1c129586a4"
url: "https://pub.dev"
source: hosted
version: "0.5.38"
version: "0.5.39"
sqlparser:
dependency: transitive
description:
@@ -2424,10 +2416,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
url: "https://pub.dev"
source: hosted
version: "6.3.16"
version: "6.3.17"
url_launcher_ios:
dependency: transitive
description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 3.1.0+118
version: 3.1.0+122
environment:
sdk: ^3.7.2
@@ -39,12 +39,12 @@ dependencies:
flutter_hooks: ^0.21.2
hooks_riverpod: ^2.6.1
bitsdojo_window: ^0.1.6
go_router: ^16.0.0
go_router: ^16.1.0
styled_widget: ^0.4.1
shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1
path_provider: ^2.1.5
dio: ^5.8.0+1
dio: ^5.9.0
very_good_infinite_list: ^0.9.0
freezed_annotation: ^3.1.0
json_annotation: ^4.9.0
@@ -73,10 +73,10 @@ dependencies:
git: https://github.com/LittleSheep2Code/tus_client.git
cross_file: ^0.3.4+2
image_picker: ^1.1.2
file_picker: ^10.2.1
file_picker: ^10.2.3
riverpod_annotation: ^2.6.1
image_picker_platform_interface: ^2.10.1
image_picker_android: ^0.8.12+24
image_picker_android: ^0.8.12+25
super_context_menu: ^0.9.1
modal_bottom_sheet: ^3.0.0
firebase_messaging: ^16.0.0
@@ -133,6 +133,7 @@ dependencies:
flutter_typeahead: ^5.2.0
flutter_langdetect: ^0.0.2
waveform_flutter: ^1.2.0
flutter_app_update: ^3.2.2
dev_dependencies:
flutter_test:
@@ -144,7 +145,6 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
auto_route_generator: ^10.1.0
build_runner: ^2.5.4
freezed: ^3.1.0
json_serializable: ^6.9.5