Compare commits

..

5 Commits

Author SHA1 Message Date
b0c1981c9a 🐛 Fix android building 2025-05-29 21:59:27 +08:00
d943275ed5 🔨 Upgrade android gradle project 2025-05-29 21:40:34 +08:00
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
39 changed files with 1141 additions and 523 deletions

View File

@@ -11,7 +11,7 @@ plugins {
android { android {
namespace = "dev.solsynth.solian" namespace = "dev.solsynth.solian"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973" ndkVersion = "29.0.13113456"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17

View File

@@ -34,19 +34,22 @@
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider <provider
android:name="com.superlist.super_native_extensions.DataProvider" android:name="androidx.core.content.FileProvider"
android:authorities="dev.solsynth.solian.SuperClipboardDataProvider" android:authorities="dev.solsynth.solian.provider"
android:exported="true" android:exported="false"
android:grantUriPermissions="true" > android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider> </provider>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@@ -61,8 +64,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain" />
</intent> </intent>
</queries> </queries>
</manifest> </manifest>

View File

@@ -1,5 +0,0 @@
package dev.solsynth.solian
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,14 @@
package dev.solsynth.solian
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
class MainActivity : FlutterActivity()
{
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362
flutterEngine.plugins.add(LegacySharedPreferencesPlugin())
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external_files"
path="." />
</paths>

File diff suppressed because one or more lines are too long

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false id("com.android.application") version "8.10.1" apply false
// START: FlutterFire Configuration // START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration // END: FlutterFire Configuration

View File

@@ -308,5 +308,7 @@
"accountDataExportRequested": "Data export requested. You'll receive an email when it's ready.", "accountDataExportRequested": "Data export requested. You'll receive an email when it's ready.",
"accountDeletionDescription": "Permanently delete your account and all your data", "accountDeletionDescription": "Permanently delete your account and all your data",
"accountSettingsHelp": "Account Settings Help", "accountSettingsHelp": "Account Settings Help",
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support." "accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
"unauthorized": "Unauthorized",
"unauthorizedHint": "You're not signed in or session expired, please sign in again."
} }

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

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -27,12 +28,12 @@ import 'package:island/widgets/chat/message_item.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'chat.dart'; import 'chat.dart';
import 'package:island/widgets/chat/call_button.dart'; import 'package:island/widgets/chat/call_button.dart';
@@ -117,19 +118,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,
@@ -752,33 +746,16 @@ class _ChatInput extends ConsumerWidget {
} }
Future<void> _handlePaste() async { Future<void> _handlePaste() async {
final clipboard = SystemClipboard.instance; final clipboard = await Pasteboard.image;
if (clipboard == null) return; if (clipboard == null) return;
final reader = await clipboard.read(); onAttachmentsChanged([
if (reader.canProvide(Formats.png)) { ...attachments,
reader.getFile(Formats.png, (file) async { UniversalFile(
final stream = file.getStream(); data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
final bytes = await stream.toList(); type: UniversalFileType.image,
final imageBytes = bytes.expand((e) => e).toList(); ),
]);
// Create a temporary file to store the image
final tempDir = Directory.systemTemp;
final tempFile = File(
'${tempDir.path}/pasted_image_${DateTime.now().millisecondsSinceEpoch}.png',
);
await tempFile.writeAsBytes(imageBytes);
// Add the file to attachments
onAttachmentsChanged([
...attachments,
UniversalFile(
data: XFile(tempFile.path),
type: UniversalFileType.image,
),
]);
});
}
} }
@override @override

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

@@ -23,8 +23,8 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.dart'; import 'package:island/widgets/post/publishers_modal.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:super_clipboard/super_clipboard.dart';
@RoutePage() @RoutePage()
class PostEditScreen extends HookConsumerWidget { class PostEditScreen extends HookConsumerWidget {
@@ -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:
@@ -218,33 +211,16 @@ class PostComposeScreen extends HookConsumerWidget {
} }
Future<void> _handlePaste() async { Future<void> _handlePaste() async {
final clipboard = SystemClipboard.instance; final clipboard = await Pasteboard.image;
if (clipboard == null) return; if (clipboard == null) return;
final reader = await clipboard.read(); attachments.value = [
if (reader.canProvide(Formats.png)) { ...attachments.value,
reader.getFile(Formats.png, (file) async { UniversalFile(
final stream = file.getStream(); data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
final bytes = await stream.toList(); type: UniversalFileType.image,
final imageBytes = bytes.expand((e) => e).toList(); ),
];
// Create a temporary file to store the image
final tempDir = Directory.systemTemp;
final tempFile = File(
'${tempDir.path}/pasted_image_${DateTime.now().millisecondsSinceEpoch}.png',
);
await tempFile.writeAsBytes(imageBytes);
// Add the file to attachments
attachments.value = [
...attachments.value,
UniversalFile(
data: XFile(tempFile.path),
type: UniversalFileType.image,
),
];
});
}
} }
void _handleKeyPress(RawKeyEvent event) { void _handleKeyPress(RawKeyEvent event) {
@@ -394,49 +370,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 +403,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

@@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@@ -7,11 +8,8 @@ import 'package:styled_widget/styled_widget.dart';
class ResponseErrorWidget extends StatelessWidget { class ResponseErrorWidget extends StatelessWidget {
final dynamic error; final dynamic error;
final VoidCallback onRetry; final VoidCallback onRetry;
const ResponseErrorWidget({
super.key, const ResponseErrorWidget({super.key, required this.error, required this.onRetry});
required this.error,
required this.onRetry,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -20,14 +18,33 @@ class ResponseErrorWidget extends StatelessWidget {
children: [ children: [
const Icon(Symbols.error_outline, size: 48), const Icon(Symbols.error_outline, size: 48),
const Gap(4), const Gap(4),
ConstrainedBox( if (error is DioException && error.response?.statusCode == 401)
constraints: const BoxConstraints(maxWidth: 320), ConstrainedBox(
child: Text( constraints: const BoxConstraints(maxWidth: 320),
error.toString(), child: Column(
textAlign: TextAlign.center, children: [
style: const TextStyle(color: Color(0xFF757575)), Text(
), 'unauthorized'.tr(),
).center(), textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
).bold(),
Text(
'unauthorizedHint'.tr(),
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
],
),
).center()
else
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Text(
error.toString(),
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
).center(),
const Gap(8), const Gap(8),
TextButton(onPressed: onRetry, child: const Text('retry').tr()), TextButton(onPressed: onRetry, child: const Text('retry').tr()),
], ],

View File

@@ -14,6 +14,7 @@
#include <irondash_engine_context/irondash_engine_context_plugin.h> #include <irondash_engine_context/irondash_engine_context_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h> #include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h> #include <super_native_extensions/super_native_extensions_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
@@ -44,6 +45,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) media_kit_video_registrar = g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);

View File

@@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
irondash_engine_context irondash_engine_context
media_kit_libs_linux media_kit_libs_linux
media_kit_video media_kit_video
pasteboard
sqlite3_flutter_libs sqlite3_flutter_libs
super_native_extensions super_native_extensions
url_launcher_linux url_launcher_linux

View File

@@ -21,6 +21,7 @@ import livekit_client
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video import media_kit_video
import package_info_plus import package_info_plus
import pasteboard
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
@@ -47,6 +48,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

View File

@@ -77,10 +77,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: auto_route name: auto_route
sha256: eae18fcd3e3762eb6074a3560c0f411d1e36bd9f8d3eed9c15ed1c577e8d1815 sha256: b8c036fa613a98a759cf0fdcba26e62f4985dcbff01a5e760ab411e8554bbaf0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.0" version: "10.1.0+1"
auto_route_generator: auto_route_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -205,10 +205,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.9.5" version: "8.10.1"
cached_network_image: cached_network_image:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -589,10 +589,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_macos name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.4+2" version: "0.9.4+3"
file_selector_platform_interface: file_selector_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1086,12 +1086,13 @@ packages:
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
irondash_engine_context: irondash_engine_context:
dependency: transitive dependency: "direct overridden"
description: description:
name: irondash_engine_context path: "engine_context/dart"
sha256: cd7b769db11a2b5243b037c8a9b1ecaef02e1ae27a2d909ffa78c1dad747bb10 ref: "refs/pull/66/head"
url: "https://pub.dev" resolved-ref: e2551d9a3b0272a723b3627c5ef70e01a9e26961
source: hosted url: "https://github.com/irondash/irondash.git"
source: git
version: "0.5.4" version: "0.5.4"
irondash_message_channel: irondash_message_channel:
dependency: transitive dependency: transitive
@@ -1373,6 +1374,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.2.0"
pasteboard:
dependency: "direct main"
description:
name: pasteboard
sha256: "9ff73ada33f79a59ff91f6c01881fd4ed0a0031cfc4ae2d86c0384471525fca1"
url: "https://pub.dev"
source: hosted
version: "0.4.0"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1874,14 +1883,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.1" version: "0.4.1"
super_clipboard:
dependency: "direct main"
description:
name: super_clipboard
sha256: "5203c881d24033c3e6154c2ae01afd94e7f0a3201280373f28e540f1defa3f40"
url: "https://pub.dev"
source: hosted
version: "0.9.0-dev.6"
super_context_menu: super_context_menu:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1891,12 +1892,13 @@ packages:
source: hosted source: hosted
version: "0.9.0-dev.6" version: "0.9.0-dev.6"
super_native_extensions: super_native_extensions:
dependency: transitive dependency: "direct overridden"
description: description:
name: super_native_extensions path: super_native_extensions
sha256: "09ccc40c475e6f91770eaeb2553bf4803812d7beadc3759aa57d643370619c86" ref: "refs/pull/525/head"
url: "https://pub.dev" resolved-ref: d3020a8c5acd8555707b3b6477fd744d09f3e22f
source: hosted url: "https://github.com/superlistapp/super_native_extensions.git"
source: git
version: "0.9.0-dev.6" version: "0.9.0-dev.6"
super_sliver_list: super_sliver_list:
dependency: "direct main" dependency: "direct main"

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
@@ -100,9 +100,9 @@ dependencies:
photo_view: ^0.15.0 photo_view: ^0.15.0
dismissible_page: ^1.0.2 dismissible_page: ^1.0.2
super_sliver_list: ^0.4.1 super_sliver_list: ^0.4.1
super_clipboard: ^0.9.0-dev.6
flutter_webrtc: ^0.14.1 flutter_webrtc: ^0.14.1
livekit_client: ^2.4.7 livekit_client: ^2.4.7
pasteboard: ^0.4.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -130,6 +130,17 @@ dependency_overrides:
analyzer_plugin: 0.12.0 analyzer_plugin: 0.12.0
# https://github.com/dart-lang/sdk/issues/60784#issuecomment-2906872272 # https://github.com/dart-lang/sdk/issues/60784#issuecomment-2906872272
custom_lint_visitor: 1.0.0+7.3.0 custom_lint_visitor: 1.0.0+7.3.0
# https://github.com/superlistapp/super_native_extensions/issues/524#issuecomment-2918531753
super_native_extensions:
git:
url: https://github.com/superlistapp/super_native_extensions.git
ref: refs/pull/525/head
path: super_native_extensions
irondash_engine_context:
git:
url: https://github.com/irondash/irondash.git
ref: refs/pull/66/head
path: engine_context/dart
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

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>

View File

@@ -18,6 +18,7 @@
#include <livekit_client/live_kit_plugin.h> #include <livekit_client/live_kit_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h> #include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h> #include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <pasteboard/pasteboard_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin_c_api.h> #include <super_native_extensions/super_native_extensions_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
@@ -48,6 +49,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
MediaKitVideoPluginCApiRegisterWithRegistrar( MediaKitVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
PasteboardPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PasteboardPlugin"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar( Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
SuperNativeExtensionsPluginCApiRegisterWithRegistrar( SuperNativeExtensionsPluginCApiRegisterWithRegistrar(

View File

@@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
livekit_client livekit_client
media_kit_libs_windows_video media_kit_libs_windows_video
media_kit_video media_kit_video
pasteboard
sqlite3_flutter_libs sqlite3_flutter_libs
super_native_extensions super_native_extensions
url_launcher_windows url_launcher_windows