Compare commits

..

3 Commits

Author SHA1 Message Date
010a49251c 🚀 Launch 3.0.0+96 2025-05-29 01:45:51 +08:00
bdc13978c3 👽 Support the new Dyson Token 2025-05-28 23:21:13 +08:00
5d8c73e468 🐛 Fix replies activities didn't rendered 2025-05-28 23:08:53 +08:00
23 changed files with 339 additions and 412 deletions

View File

@ -186,7 +186,7 @@ class MessageRepository {
} }
Future<LocalChatMessage> sendMessage( Future<LocalChatMessage> sendMessage(
String atk, String token,
String baseUrl, String baseUrl,
String roomId, String roomId,
String content, String content,
@ -232,7 +232,7 @@ class MessageRepository {
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: attachments[idx].data, fileData: attachments[idx].data,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: attachments[idx].data.name ?? 'Post media', filename: attachments[idx].data.name ?? 'Post media',
mimetype: mimetype:

View File

@ -4,14 +4,11 @@ part 'auth.freezed.dart';
part 'auth.g.dart'; part 'auth.g.dart';
@freezed @freezed
sealed class AppTokenPair with _$AppTokenPair { sealed class AppToken with _$AppToken {
const factory AppTokenPair({ const factory AppToken({required String token}) = _AppToken;
required String accessToken,
required String refreshToken,
}) = _AppTokenPair;
factory AppTokenPair.fromJson(Map<String, dynamic> json) => factory AppToken.fromJson(Map<String, dynamic> json) =>
_$AppTokenPairFromJson(json); _$AppTokenFromJson(json);
} }
@freezed @freezed

View File

@ -14,42 +14,42 @@ part of 'auth.dart';
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$AppTokenPair { mixin _$AppToken {
String get accessToken; String get refreshToken; String get token;
/// Create a copy of AppTokenPair /// Create a copy of AppToken
/// 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)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$AppTokenPairCopyWith<AppTokenPair> get copyWith => _$AppTokenPairCopyWithImpl<AppTokenPair>(this as AppTokenPair, _$identity); $AppTokenCopyWith<AppToken> get copyWith => _$AppTokenCopyWithImpl<AppToken>(this as AppToken, _$identity);
/// Serializes this AppTokenPair to a JSON map. /// Serializes this AppToken to a JSON map.
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppTokenPair&&(identical(other.accessToken, accessToken) || other.accessToken == accessToken)&&(identical(other.refreshToken, refreshToken) || other.refreshToken == refreshToken)); return identical(this, other) || (other.runtimeType == runtimeType&&other is AppToken&&(identical(other.token, token) || other.token == token));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,accessToken,refreshToken); int get hashCode => Object.hash(runtimeType,token);
@override @override
String toString() { String toString() {
return 'AppTokenPair(accessToken: $accessToken, refreshToken: $refreshToken)'; return 'AppToken(token: $token)';
} }
} }
/// @nodoc /// @nodoc
abstract mixin class $AppTokenPairCopyWith<$Res> { abstract mixin class $AppTokenCopyWith<$Res> {
factory $AppTokenPairCopyWith(AppTokenPair value, $Res Function(AppTokenPair) _then) = _$AppTokenPairCopyWithImpl; factory $AppTokenCopyWith(AppToken value, $Res Function(AppToken) _then) = _$AppTokenCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String accessToken, String refreshToken String token
}); });
@ -57,19 +57,18 @@ $Res call({
} }
/// @nodoc /// @nodoc
class _$AppTokenPairCopyWithImpl<$Res> class _$AppTokenCopyWithImpl<$Res>
implements $AppTokenPairCopyWith<$Res> { implements $AppTokenCopyWith<$Res> {
_$AppTokenPairCopyWithImpl(this._self, this._then); _$AppTokenCopyWithImpl(this._self, this._then);
final AppTokenPair _self; final AppToken _self;
final $Res Function(AppTokenPair) _then; final $Res Function(AppToken) _then;
/// Create a copy of AppTokenPair /// Create a copy of AppToken
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? accessToken = null,Object? refreshToken = null,}) { @pragma('vm:prefer-inline') @override $Res call({Object? token = null,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
accessToken: null == accessToken ? _self.accessToken : accessToken // ignore: cast_nullable_to_non_nullable token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,refreshToken: null == refreshToken ? _self.refreshToken : refreshToken // ignore: cast_nullable_to_non_nullable
as String, as String,
)); ));
} }
@ -80,47 +79,46 @@ as String,
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _AppTokenPair implements AppTokenPair { class _AppToken implements AppToken {
const _AppTokenPair({required this.accessToken, required this.refreshToken}); const _AppToken({required this.token});
factory _AppTokenPair.fromJson(Map<String, dynamic> json) => _$AppTokenPairFromJson(json); factory _AppToken.fromJson(Map<String, dynamic> json) => _$AppTokenFromJson(json);
@override final String accessToken; @override final String token;
@override final String refreshToken;
/// Create a copy of AppTokenPair /// Create a copy of AppToken
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false) @override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$AppTokenPairCopyWith<_AppTokenPair> get copyWith => __$AppTokenPairCopyWithImpl<_AppTokenPair>(this, _$identity); _$AppTokenCopyWith<_AppToken> get copyWith => __$AppTokenCopyWithImpl<_AppToken>(this, _$identity);
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$AppTokenPairToJson(this, ); return _$AppTokenToJson(this, );
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppTokenPair&&(identical(other.accessToken, accessToken) || other.accessToken == accessToken)&&(identical(other.refreshToken, refreshToken) || other.refreshToken == refreshToken)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppToken&&(identical(other.token, token) || other.token == token));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,accessToken,refreshToken); int get hashCode => Object.hash(runtimeType,token);
@override @override
String toString() { String toString() {
return 'AppTokenPair(accessToken: $accessToken, refreshToken: $refreshToken)'; return 'AppToken(token: $token)';
} }
} }
/// @nodoc /// @nodoc
abstract mixin class _$AppTokenPairCopyWith<$Res> implements $AppTokenPairCopyWith<$Res> { abstract mixin class _$AppTokenCopyWith<$Res> implements $AppTokenCopyWith<$Res> {
factory _$AppTokenPairCopyWith(_AppTokenPair value, $Res Function(_AppTokenPair) _then) = __$AppTokenPairCopyWithImpl; factory _$AppTokenCopyWith(_AppToken value, $Res Function(_AppToken) _then) = __$AppTokenCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String accessToken, String refreshToken String token
}); });
@ -128,19 +126,18 @@ $Res call({
} }
/// @nodoc /// @nodoc
class __$AppTokenPairCopyWithImpl<$Res> class __$AppTokenCopyWithImpl<$Res>
implements _$AppTokenPairCopyWith<$Res> { implements _$AppTokenCopyWith<$Res> {
__$AppTokenPairCopyWithImpl(this._self, this._then); __$AppTokenCopyWithImpl(this._self, this._then);
final _AppTokenPair _self; final _AppToken _self;
final $Res Function(_AppTokenPair) _then; final $Res Function(_AppToken) _then;
/// Create a copy of AppTokenPair /// Create a copy of AppToken
/// 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? accessToken = null,Object? refreshToken = null,}) { @override @pragma('vm:prefer-inline') $Res call({Object? token = null,}) {
return _then(_AppTokenPair( return _then(_AppToken(
accessToken: null == accessToken ? _self.accessToken : accessToken // ignore: cast_nullable_to_non_nullable token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,refreshToken: null == refreshToken ? _self.refreshToken : refreshToken // ignore: cast_nullable_to_non_nullable
as String, as String,
)); ));
} }

View File

@ -6,17 +6,12 @@ part of 'auth.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_AppTokenPair _$AppTokenPairFromJson(Map<String, dynamic> json) => _AppToken _$AppTokenFromJson(Map<String, dynamic> json) =>
_AppTokenPair( _AppToken(token: json['token'] as String);
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
);
Map<String, dynamic> _$AppTokenPairToJson(_AppTokenPair instance) => Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{
<String, dynamic>{ 'token': instance.token,
'access_token': instance.accessToken, };
'refresh_token': instance.refreshToken,
};
_SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
_SnAuthChallenge( _SnAuthChallenge(

View File

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -68,16 +67,9 @@ final apiClientProvider = Provider<Dio>((ref) {
RequestInterceptorHandler handler, RequestInterceptorHandler handler,
) async { ) async {
try { try {
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token != null) {
ref.watch(serverUrlProvider), options.headers['Authorization'] = 'AtField $token';
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk != null) {
options.headers['Authorization'] = 'Bearer $atk';
} }
} catch (err) { } catch (err) {
// ignore // ignore
@ -95,105 +87,21 @@ final apiClientProvider = Provider<Dio>((ref) {
return dio; return dio;
}); });
final tokenPairProvider = Provider<AppTokenPair?>((ref) { final tokenProvider = Provider<AppToken?>((ref) {
final prefs = ref.watch(sharedPreferencesProvider); final prefs = ref.watch(sharedPreferencesProvider);
final tkPairString = prefs.getString(kTokenPairStoreKey); final tokenString = prefs.getString(kTokenPairStoreKey);
if (tkPairString == null) return null; if (tokenString == null) return null;
return AppTokenPair.fromJson(jsonDecode(tkPairString)); return AppToken.fromJson(jsonDecode(tokenString));
}); });
Future<(String, String)?> refreshToken(String baseUrl, String? rtk) async { // Token refresh functionality removed as per backend changes
if (rtk == null) return null;
final dio = Dio(); Future<String?> getToken(AppToken? token) async {
dio.options.baseUrl = baseUrl; return token?.token;
final resp = await dio.post(
'/auth/token',
data: {'grant_type': 'refresh_token', 'refresh_token': rtk},
);
final String atk = resp.data['access_token'];
final String nRtk = resp.data['refresh_token'];
return (atk, nRtk);
} }
Completer<String?>? _refreshCompleter; Future<void> setToken(SharedPreferences prefs, String token) async {
final appToken = AppToken(token: token);
Future<String?> getFreshAtk( final tokenString = jsonEncode(appToken);
AppTokenPair? tkPair, prefs.setString(kTokenPairStoreKey, tokenString);
String baseUrl, {
Function(String, String)? onRefreshed,
}) async {
var atk = tkPair?.accessToken;
var rtk = tkPair?.refreshToken;
if (_refreshCompleter != null) {
return await _refreshCompleter!.future;
} else {
_refreshCompleter = Completer<String?>();
}
try {
if (atk != null) {
final atkParts = atk.split('.');
if (atkParts.length != 3) {
throw Exception('invalid format of access token');
}
var rawPayload = atkParts[1].replaceAll('-', '+').replaceAll('_', '/');
switch (rawPayload.length % 4) {
case 0:
break;
case 2:
rawPayload += '==';
break;
case 3:
rawPayload += '=';
break;
default:
throw Exception('illegal format of access token payload');
}
final b64 = utf8.fuse(base64Url);
final payload = b64.decode(rawPayload);
final exp = jsonDecode(payload)['exp'];
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
log('[Auth] Access token need refresh, doing it at ${DateTime.now()}');
final result = await refreshToken(baseUrl, rtk);
if (result == null) {
atk = null;
} else {
onRefreshed?.call(result.$1, result.$2);
atk = result.$1;
}
}
if (atk != null) {
_refreshCompleter!.complete(atk);
return atk;
} else {
log('[Auth] Access token refresh failed...');
_refreshCompleter!.complete(null);
}
}
} catch (err) {
log('[Auth] Failed to authenticate user... $err');
_refreshCompleter!.completeError(err);
} finally {
_refreshCompleter = null;
}
return null;
}
Future<void> setTokenPair(
SharedPreferences prefs,
String atk,
String rtk,
) async {
final tkPair = AppTokenPair(accessToken: atk, refreshToken: rtk);
final tkPairString = jsonEncode(tkPair);
prefs.setString(kTokenPairStoreKey, tkPairString);
} }

View File

@ -13,7 +13,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
Future<String?> getAccessToken() async { Future<String?> getAccessToken() async {
final prefs = _ref.read(sharedPreferencesProvider); final prefs = _ref.read(sharedPreferencesProvider);
return prefs.getString('dyn_user_atk'); return prefs.getString(kTokenPairStoreKey);
} }
Future<void> fetchUser() async { Future<void> fetchUser() async {

View File

@ -54,25 +54,18 @@ class WebSocketService {
_statusStreamController.sink.add(WebSocketState.connecting()); _statusStreamController.sink.add(WebSocketState.connecting());
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
final url = '$baseUrl/ws'.replaceFirst('http', 'ws'); final url = '$baseUrl/ws'.replaceFirst('http', 'ws');
log('[WebSocket] Trying connecting to $url'); log('[WebSocket] Trying connecting to $url');
try { try {
if (kIsWeb) { if (kIsWeb) {
_channel = WebSocketChannel.connect(Uri.parse('$url?tk=$atk')); _channel = WebSocketChannel.connect(Uri.parse('$url?tk=$token'));
} else { } else {
_channel = IOWebSocketChannel.connect( _channel = IOWebSocketChannel.connect(
Uri.parse(url), Uri.parse(url),
headers: {'Authorization': 'Bearer $atk'}, headers: {'Authorization': 'Bearer $token'},
); );
} }
await _channel!.ready; await _channel!.ready;

View File

@ -254,8 +254,8 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('Copy access token'), title: Text('Copy access token'),
onTap: () async { onTap: () async {
final tk = ref.watch(tokenPairProvider); final tk = ref.watch(tokenProvider);
Clipboard.setData(ClipboardData(text: tk!.accessToken)); Clipboard.setData(ClipboardData(text: tk!.token));
}, },
), ),
if (kDebugMode) if (kDebugMode)

View File

@ -59,19 +59,12 @@ class UpdateProfileScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: result,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg', mimetype: result.mimeType ?? 'image/jpeg',

View File

@ -142,10 +142,9 @@ class _LoginCheckScreen extends HookConsumerWidget {
'/auth/token', '/auth/token',
data: {'grant_type': 'authorization_code', 'code': result.id}, data: {'grant_type': 'authorization_code', 'code': result.id},
); );
final atk = tokenResp.data['access_token']; final token = tokenResp.data['token'];
final rtk = tokenResp.data['refresh_token']; setToken(ref.watch(sharedPreferencesProvider), token);
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk); ref.invalidate(tokenProvider);
ref.invalidate(tokenPairProvider);
if (!context.mounted) return; if (!context.mounted) return;
final userNotifier = ref.read(userInfoProvider.notifier); final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser().then((_) { userNotifier.fetchUser().then((_) {

View File

@ -522,19 +522,12 @@ class EditChatScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: result,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg', mimetype: result.mimeType ?? 'image/jpeg',

View File

@ -117,19 +117,12 @@ class MessagesNotifier extends _$MessagesNotifier {
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
final baseUrl = ref.read(serverUrlProvider); final baseUrl = ref.read(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Access token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
await repository.sendMessage( await repository.sendMessage(
atk, token,
baseUrl, baseUrl,
_roomId, _roomId,
content, content,

View File

@ -6,7 +6,7 @@ part of 'room.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$messagesNotifierHash() => r'71a9fc1c6d024f6203f06225384c19335b9b6f2c'; String _$messagesNotifierHash() => r'afc4d43f4948ec571118cef0321838a6cefc89c0';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@ -95,19 +95,12 @@ class EditPublisherScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: result,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg', mimetype: result.mimeType ?? 'image/jpeg',

View File

@ -17,6 +17,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart'; part 'explore.g.dart';
@ -131,10 +132,16 @@ class _ActivityListView extends HookConsumerWidget {
switch (item.type) { switch (item.type) {
case 'posts.new': case 'posts.new':
case 'posts.new.replies':
final isReply = item.type == 'posts.new.replies';
itemWidget = PostItem( itemWidget = PostItem(
backgroundColor: backgroundColor:
isWideScreen(context) ? Colors.transparent : null, isWideScreen(context) ? Colors.transparent : null,
item: SnPost.fromJson(item.data), item: SnPost.fromJson(item.data),
padding:
isReply
? EdgeInsets.only(left: 16, right: 16, bottom: 16)
: null,
onRefresh: (_) { onRefresh: (_) {
activitiesNotifier.forceRefresh(); activitiesNotifier.forceRefresh();
}, },
@ -145,6 +152,21 @@ class _ActivityListView extends HookConsumerWidget {
); );
}, },
); );
if (isReply) {
itemWidget = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Icon(Symbols.reply),
const Gap(8),
Text('Replying your post'),
],
).padding(horizontal: 20, vertical: 8),
itemWidget,
],
);
}
break; break;
case 'accounts.check-in': case 'accounts.check-in':
itemWidget = CheckInActivityWidget(item: item); itemWidget = CheckInActivityWidget(item: item);

View File

@ -7,7 +7,7 @@ part of 'notification.dart';
// ************************************************************************** // **************************************************************************
String _$notificationUnreadCountNotifierHash() => String _$notificationUnreadCountNotifierHash() =>
r'074143cf208a3afe1495be405198532a23ef77c8'; r'372a2cc259d7d838cd4f33a9129f7396ef31dbb9';
/// See also [NotificationUnreadCountNotifier]. /// See also [NotificationUnreadCountNotifier].
@ProviderFor(NotificationUnreadCountNotifier) @ProviderFor(NotificationUnreadCountNotifier)

View File

@ -125,21 +125,14 @@ class PostComposeScreen extends HookConsumerWidget {
final attachment = attachments.value[index]; final attachment = attachments.value[index];
if (attachment is SnCloudFile) return; if (attachment is SnCloudFile) return;
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
try { try {
attachmentProgress.value = {...attachmentProgress.value, index: 0}; attachmentProgress.value = {...attachmentProgress.value, index: 0};
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: attachment.data, fileData: attachment.data,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: attachment.data.name ?? 'Post media', filename: attachment.data.name ?? 'Post media',
mimetype: mimetype:
@ -394,49 +387,32 @@ class PostComposeScreen extends HookConsumerWidget {
final isWide = isWideScreen(context); final isWide = isWideScreen(context);
return isWide return isWide
? Wrap( ? Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: [
for (var idx = 0; idx < attachments.value.length; idx++) for (
SizedBox( var idx = 0;
width: constraints.maxWidth / 2 - 4, idx < attachments.value.length;
child: AttachmentPreview( idx++
item: attachments.value[idx], )
progress: attachmentProgress.value[idx], SizedBox(
onRequestUpload: () => uploadAttachment(idx), width: constraints.maxWidth / 2 - 4,
onDelete: () => deleteAttachment(idx), child: AttachmentPreview(
onMove: (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;
},
),
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
for (var idx = 0; idx < attachments.value.length; idx++)
AttachmentPreview(
item: attachments.value[idx], item: attachments.value[idx],
progress: attachmentProgress.value[idx], progress:
onRequestUpload: () => uploadAttachment(idx), attachmentProgress.value[idx],
onRequestUpload:
() => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx), onDelete: () => deleteAttachment(idx),
onMove: (delta) { onMove: (delta) {
if (idx + delta < 0 || if (idx + delta < 0 ||
idx + delta >= attachments.value.length) { idx + delta >=
attachments.value.length) {
return; return;
} }
final clone = List.of(attachments.value); final clone = List.of(
attachments.value,
);
clone.insert( clone.insert(
idx + delta, idx + delta,
clone.removeAt(idx), clone.removeAt(idx),
@ -444,8 +420,42 @@ class PostComposeScreen extends HookConsumerWidget {
attachments.value = clone; attachments.value = clone;
}, },
), ),
], ),
); ],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
for (
var idx = 0;
idx < attachments.value.length;
idx++
)
AttachmentPreview(
item: attachments.value[idx],
progress: attachmentProgress.value[idx],
onRequestUpload:
() => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (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;
},
),
],
);
}, },
), ),
], ],

View File

@ -145,7 +145,7 @@ class _PublisherProviderElement
String get uname => (origin as PublisherProvider).uname; String get uname => (origin as PublisherProvider).uname;
} }
String _$publisherBadgesHash() => r'b26d8804ddc9734c453bdf76af0a9336f166542c'; String _$publisherBadgesHash() => r'a5781deded7e682a781ccd7854418f050438e3f4';
/// See also [publisherBadges]. /// See also [publisherBadges].
@ProviderFor(publisherBadges) @ProviderFor(publisherBadges)

View File

@ -211,19 +211,12 @@ class EditRealmScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Access token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: result,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg', mimetype: result.mimeType ?? 'image/jpeg',

View File

@ -43,15 +43,8 @@ class CloudFilePicker extends HookConsumerWidget {
if (files.value.isEmpty) return; if (files.value.isEmpty) return;
final baseUrl = ref.read(serverUrlProvider); final baseUrl = ref.read(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw Exception("Unauthorized");
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw Exception("Unauthorized");
List<SnCloudFile> result = List.empty(growable: true); List<SnCloudFile> result = List.empty(growable: true);
@ -64,7 +57,7 @@ class CloudFilePicker extends HookConsumerWidget {
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: file.data, fileData: file.data,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: file.data.name ?? 'Post media', filename: file.data.name ?? 'Post media',
mimetype: mimetype:

View File

@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
@ -38,18 +37,10 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url); final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) { if (inCacheInfo == null) {
log('[MediaPlayer] Miss cache: $url'); log('[MediaPlayer] Miss cache: $url');
final baseUrl = ref.watch(serverUrlProvider); final token = await getToken(ref.watch(tokenProvider));
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
final fileStream = DefaultCacheManager().getFileStream( final fileStream = DefaultCacheManager().getFileStream(
url, url,
headers: {'Authorization': 'Bearer $atk'}, headers: {'Authorization': 'Bearer $token'},
withProgress: true, withProgress: true,
); );
await for (var fileInfo in fileStream) { await for (var fileInfo in fileStream) {

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 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 3.0.0+95 version: 3.0.0+96
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2

View File

@ -1,4 +1,6 @@
<!DOCTYPE html><html><head> <!doctype html>
<html>
<head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
@ -12,27 +14,30 @@
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="The Solar Network, an open-source social network."> <meta
name="description"
content="The Solar Network, an open-source social network."
/>
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Solar Network"> <meta name="apple-mobile-web-app-title" content="Solar Network" />
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png" />
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"> <link rel="icon" type="image/png" href="favicon.png" />
<title>Solar Network</title> <title>Solar Network</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json" />
<style> <style>
@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..900;1,300..900&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..900;1,300..900&display=swap");
html, html,
body { body {
font-family: "Rubik", sans-serif; font-family: "Rubik", sans-serif;
@ -77,7 +82,7 @@
} }
.swal-button--confirm { .swal-button--confirm {
background-color: #6750A4; background-color: #6750a4;
color: #ffffff; color: #ffffff;
} }
@ -87,7 +92,7 @@
.swal-button--cancel { .swal-button--cancel {
background-color: transparent; background-color: transparent;
color: #6750A4; color: #6750a4;
} }
.swal-button--cancel:hover { .swal-button--cancel:hover {
@ -95,33 +100,33 @@
} }
.swal-icon { .swal-icon {
border-color: #6750A4; border-color: #6750a4;
margin: 12px auto; margin: 12px auto;
} }
.swal-icon--success__line { .swal-icon--success__line {
background-color: #6750A4; background-color: #6750a4;
} }
.swal-icon--success__ring { .swal-icon--success__ring {
border-color: #6750A4; border-color: #6750a4;
} }
.swal-icon--warning { .swal-icon--warning {
border-color: #F2B824; border-color: #f2b824;
} }
.swal-icon--warning__body, .swal-icon--warning__body,
.swal-icon--warning__dot { .swal-icon--warning__dot {
background-color: #F2B824; background-color: #f2b824;
} }
.swal-icon--error { .swal-icon--error {
border-color: #DC362E; border-color: #dc362e;
} }
.swal-icon--error__line { .swal-icon--error__line {
background-color: #DC362E; background-color: #dc362e;
} }
.swal-footer { .swal-footer {
@ -130,92 +135,144 @@
border: none; border: none;
text-align: right; text-align: right;
} }
</style> </style>
<style id="splash-screen-style"> <style id="splash-screen-style">
html { html {
height: 100% height: 100%;
} }
body {
margin: 0;
min-height: 100%;
background-color: #ffffff;
background-size: 100% 100%;
}
.center {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.contain {
display:block;
width:100%; height:100%;
object-fit: contain;
}
.stretch {
display:block;
width:100%; height:100%;
}
.cover {
display:block;
width:100%; height:100%;
object-fit: cover;
}
.bottom {
position: absolute;
bottom: 0;
left: 50%;
-ms-transform: translate(-50%, 0);
transform: translate(-50%, 0);
}
.bottomLeft {
position: absolute;
bottom: 0;
left: 0;
}
.bottomRight {
position: absolute;
bottom: 0;
right: 0;
}
@media (prefers-color-scheme: dark) {
body { body {
background-color: #121212; margin: 0;
} min-height: 100%;
} background-color: #ffffff;
</style> background-size: 100% 100%;
<script id="splash-screen-script"> }
function removeSplashFromWeb() {
document.getElementById("splash")?.remove(); .center {
document.getElementById("splash-branding")?.remove(); margin: 0;
document.body.style.background = "transparent"; position: absolute;
} top: 50%;
</script> left: 50%;
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> -ms-transform: translate(-50%, -50%);
</head> transform: translate(-50%, -50%);
}
.contain {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.stretch {
display: block;
width: 100%;
height: 100%;
}
.cover {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.bottom {
position: absolute;
bottom: 0;
left: 50%;
-ms-transform: translate(-50%, 0);
transform: translate(-50%, 0);
}
.bottomLeft {
position: absolute;
bottom: 0;
left: 0;
}
.bottomRight {
position: absolute;
bottom: 0;
right: 0;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
}
}
</style>
<script id="splash-screen-script">
function removeSplashFromWeb() {
document.getElementById("splash")?.remove();
document.getElementById("splash-branding")?.remove();
document.body.style.background = "transparent";
}
</script>
<meta
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
name="viewport"
/>
</head>
<body> <body>
<picture id="splash"> <picture id="splash">
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)"> <source
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)"> srcset="
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt=""> splash/img/light-1x.png 1x,
</picture> splash/img/light-2x.png 2x,
<script src="https://unpkg.com/sweetalert@2.1.2/dist/sweetalert.min.js" async=""></script> splash/img/light-3x.png 3x,
<script src="flutter_bootstrap.js" async=""></script> splash/img/light-4x.png 4x
"
media="(prefers-color-scheme: light)"
/>
<source
srcset="
splash/img/dark-1x.png 1x,
splash/img/dark-2x.png 2x,
splash/img/dark-3x.png 3x,
splash/img/dark-4x.png 4x
"
media="(prefers-color-scheme: dark)"
/>
<img
class="center"
aria-hidden="true"
src="splash/img/light-1x.png"
alt=""
/>
</picture>
<script
src="https://unpkg.com/sweetalert@2.1.2/dist/sweetalert.min.js"
async=""
></script>
<script> <script>
document.oncontextmenu = (evt) => evt.preventDefault(); document.oncontextmenu = (evt) => evt.preventDefault();
</script> </script>
</body></html> <script>
{{flutter_js}}
{{flutter_build_config}}
const searchParams = new URLSearchParams(window.location.search);
const renderer = searchParams.get("renderer");
let cdn = searchParams.get("cdn");
if (cdn) {
localStorage.setItem("sn-web-canvaskit-cdn", cdn);
} else {
const storagedCdn = localStorage.getItem("sn-web-canvaskit-cdn");
cdn = storagedCdn ?? "com";
}
_flutter.loader.load({
config: {
renderer: renderer ?? "canvaskit",
canvasKitVariant: "full",
canvasKitBaseUrl: `https://www.gstatic.${cdn}/flutter-canvaskit/f73bfc4522dd0bc87bbcdb4bb3088082755c5e87`,
},
});
</script>
</body>
</html>