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 {
namespace = "dev.solsynth.solian"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
ndkVersion = "29.0.13113456"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17

View File

@@ -34,19 +34,22 @@
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="com.superlist.super_native_extensions.DataProvider"
android:authorities="dev.solsynth.solian.SuperClipboardDataProvider"
android:exported="true"
android:grantUriPermissions="true" >
android:name="androidx.core.content.FileProvider"
android:authorities="dev.solsynth.solian.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<!-- Don't delete the meta-data below.
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. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</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
zipStoreBase=GRADLE_USER_HOME
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 {
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
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration

View File

@@ -308,5 +308,7 @@
"accountDataExportRequested": "Data export requested. You'll receive an email when it's ready.",
"accountDeletionDescription": "Permanently delete your account and all your data",
"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(
String atk,
String token,
String baseUrl,
String roomId,
String content,
@@ -232,7 +232,7 @@ class MessageRepository {
final cloudFile =
await putMediaToCloud(
fileData: attachments[idx].data,
atk: atk,
atk: token,
baseUrl: baseUrl,
filename: attachments[idx].data.name ?? 'Post media',
mimetype:

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
@@ -68,16 +67,9 @@ final apiClientProvider = Provider<Dio>((ref) {
RequestInterceptorHandler handler,
) async {
try {
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
ref.watch(serverUrlProvider),
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk != null) {
options.headers['Authorization'] = 'Bearer $atk';
final token = await getToken(ref.watch(tokenProvider));
if (token != null) {
options.headers['Authorization'] = 'AtField $token';
}
} catch (err) {
// ignore
@@ -95,105 +87,21 @@ final apiClientProvider = Provider<Dio>((ref) {
return dio;
});
final tokenPairProvider = Provider<AppTokenPair?>((ref) {
final tokenProvider = Provider<AppToken?>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
final tkPairString = prefs.getString(kTokenPairStoreKey);
if (tkPairString == null) return null;
return AppTokenPair.fromJson(jsonDecode(tkPairString));
final tokenString = prefs.getString(kTokenPairStoreKey);
if (tokenString == null) return null;
return AppToken.fromJson(jsonDecode(tokenString));
});
Future<(String, String)?> refreshToken(String baseUrl, String? rtk) async {
if (rtk == null) return null;
// Token refresh functionality removed as per backend changes
final dio = Dio();
dio.options.baseUrl = baseUrl;
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);
Future<String?> getToken(AppToken? token) async {
return token?.token;
}
Completer<String?>? _refreshCompleter;
Future<String?> getFreshAtk(
AppTokenPair? tkPair,
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);
Future<void> setToken(SharedPreferences prefs, String token) async {
final appToken = AppToken(token: token);
final tokenString = jsonEncode(appToken);
prefs.setString(kTokenPairStoreKey, tokenString);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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/response.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:uuid/uuid.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'chat.dart';
import 'package:island/widgets/chat/call_button.dart';
@@ -117,19 +118,12 @@ class MessagesNotifier extends _$MessagesNotifier {
messageRepositoryProvider(_roomId).future,
);
final baseUrl = ref.read(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Access token is null');
final currentMessages = state.value ?? [];
await repository.sendMessage(
atk,
token,
baseUrl,
_roomId,
content,
@@ -752,33 +746,16 @@ class _ChatInput extends ConsumerWidget {
}
Future<void> _handlePaste() async {
final clipboard = SystemClipboard.instance;
final clipboard = await Pasteboard.image;
if (clipboard == null) return;
final reader = await clipboard.read();
if (reader.canProvide(Formats.png)) {
reader.getFile(Formats.png, (file) async {
final stream = file.getStream();
final bytes = await stream.toList();
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,
),
]);
});
}
onAttachmentsChanged([
...attachments,
UniversalFile(
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
type: UniversalFileType.image,
),
]);
}
@override

View File

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

View File

@@ -95,19 +95,12 @@ class EditPublisherScreen extends HookConsumerWidget {
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: result,
atk: atk,
atk: token,
baseUrl: baseUrl,
filename: result.name,
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_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.dart';
import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart';
@@ -131,10 +132,16 @@ class _ActivityListView extends HookConsumerWidget {
switch (item.type) {
case 'posts.new':
case 'posts.new.replies':
final isReply = item.type == 'posts.new.replies';
itemWidget = PostItem(
backgroundColor:
isWideScreen(context) ? Colors.transparent : null,
item: SnPost.fromJson(item.data),
padding:
isReply
? EdgeInsets.only(left: 16, right: 16, bottom: 16)
: null,
onRefresh: (_) {
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;
case 'accounts.check-in':
itemWidget = CheckInActivityWidget(item: item);

View File

@@ -7,7 +7,7 @@ part of 'notification.dart';
// **************************************************************************
String _$notificationUnreadCountNotifierHash() =>
r'074143cf208a3afe1495be405198532a23ef77c8';
r'372a2cc259d7d838cd4f33a9129f7396ef31dbb9';
/// See also [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/post/publishers_modal.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_clipboard/super_clipboard.dart';
@RoutePage()
class PostEditScreen extends HookConsumerWidget {
@@ -125,21 +125,14 @@ class PostComposeScreen extends HookConsumerWidget {
final attachment = attachments.value[index];
if (attachment is SnCloudFile) return;
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
try {
attachmentProgress.value = {...attachmentProgress.value, index: 0};
final cloudFile =
await putMediaToCloud(
fileData: attachment.data,
atk: atk,
atk: token,
baseUrl: baseUrl,
filename: attachment.data.name ?? 'Post media',
mimetype:
@@ -218,33 +211,16 @@ class PostComposeScreen extends HookConsumerWidget {
}
Future<void> _handlePaste() async {
final clipboard = SystemClipboard.instance;
final clipboard = await Pasteboard.image;
if (clipboard == null) return;
final reader = await clipboard.read();
if (reader.canProvide(Formats.png)) {
reader.getFile(Formats.png, (file) async {
final stream = file.getStream();
final bytes = await stream.toList();
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,
),
];
});
}
attachments.value = [
...attachments.value,
UniversalFile(
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
type: UniversalFileType.image,
),
];
}
void _handleKeyPress(RawKeyEvent event) {
@@ -394,49 +370,32 @@ class PostComposeScreen extends HookConsumerWidget {
final isWide = isWideScreen(context);
return isWide
? Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var idx = 0; idx < attachments.value.length; idx++)
SizedBox(
width: constraints.maxWidth / 2 - 4,
child: 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;
},
),
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
for (var idx = 0; idx < attachments.value.length; idx++)
AttachmentPreview(
spacing: 8,
runSpacing: 8,
children: [
for (
var idx = 0;
idx < attachments.value.length;
idx++
)
SizedBox(
width: constraints.maxWidth / 2 - 4,
child: AttachmentPreview(
item: attachments.value[idx],
progress: attachmentProgress.value[idx],
onRequestUpload: () => uploadAttachment(idx),
progress:
attachmentProgress.value[idx],
onRequestUpload:
() => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
idx + delta >=
attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
final clone = List.of(
attachments.value,
);
clone.insert(
idx + delta,
clone.removeAt(idx),
@@ -444,8 +403,42 @@ class PostComposeScreen extends HookConsumerWidget {
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 _$publisherBadgesHash() => r'b26d8804ddc9734c453bdf76af0a9336f166542c';
String _$publisherBadgesHash() => r'a5781deded7e682a781ccd7854418f050438e3f4';
/// See also [publisherBadges].
@ProviderFor(publisherBadges)

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:media_kit/media_kit.dart';
@@ -38,18 +37,10 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) {
log('[MediaPlayer] Miss cache: $url');
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
final token = await getToken(ref.watch(tokenProvider));
final fileStream = DefaultCacheManager().getFileStream(
url,
headers: {'Authorization': 'Bearer $atk'},
headers: {'Authorization': 'Bearer $token'},
withProgress: true,
);
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:flutter/material.dart';
import 'package:gap/gap.dart';
@@ -7,11 +8,8 @@ import 'package:styled_widget/styled_widget.dart';
class ResponseErrorWidget extends StatelessWidget {
final dynamic error;
final VoidCallback onRetry;
const ResponseErrorWidget({
super.key,
required this.error,
required this.onRetry,
});
const ResponseErrorWidget({super.key, required this.error, required this.onRetry});
@override
Widget build(BuildContext context) {
@@ -20,14 +18,33 @@ class ResponseErrorWidget extends StatelessWidget {
children: [
const Icon(Symbols.error_outline, size: 48),
const Gap(4),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Text(
error.toString(),
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
).center(),
if (error is DioException && error.response?.statusCode == 401)
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Column(
children: [
Text(
'unauthorized'.tr(),
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),
TextButton(onPressed: onRetry, child: const Text('retry').tr()),
],

View File

@@ -14,6 +14,7 @@
#include <irondash_engine_context/irondash_engine_context_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_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 <super_native_extensions/super_native_extensions_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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
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
media_kit_libs_linux
media_kit_video
pasteboard
sqlite3_flutter_libs
super_native_extensions
url_launcher_linux

View File

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

View File

@@ -77,10 +77,10 @@ packages:
dependency: "direct main"
description:
name: auto_route
sha256: eae18fcd3e3762eb6074a3560c0f411d1e36bd9f8d3eed9c15ed1c577e8d1815
sha256: b8c036fa613a98a759cf0fdcba26e62f4985dcbff01a5e760ab411e8554bbaf0
url: "https://pub.dev"
source: hosted
version: "10.1.0"
version: "10.1.0+1"
auto_route_generator:
dependency: "direct dev"
description:
@@ -205,10 +205,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
url: "https://pub.dev"
source: hosted
version: "8.9.5"
version: "8.10.1"
cached_network_image:
dependency: "direct main"
description:
@@ -589,10 +589,10 @@ packages:
dependency: transitive
description:
name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
version: "0.9.4+3"
file_selector_platform_interface:
dependency: transitive
description:
@@ -1086,12 +1086,13 @@ packages:
source: hosted
version: "1.0.5"
irondash_engine_context:
dependency: transitive
dependency: "direct overridden"
description:
name: irondash_engine_context
sha256: cd7b769db11a2b5243b037c8a9b1ecaef02e1ae27a2d909ffa78c1dad747bb10
url: "https://pub.dev"
source: hosted
path: "engine_context/dart"
ref: "refs/pull/66/head"
resolved-ref: e2551d9a3b0272a723b3627c5ef70e01a9e26961
url: "https://github.com/irondash/irondash.git"
source: git
version: "0.5.4"
irondash_message_channel:
dependency: transitive
@@ -1373,6 +1374,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.0"
pasteboard:
dependency: "direct main"
description:
name: pasteboard
sha256: "9ff73ada33f79a59ff91f6c01881fd4ed0a0031cfc4ae2d86c0384471525fca1"
url: "https://pub.dev"
source: hosted
version: "0.4.0"
path:
dependency: "direct main"
description:
@@ -1874,14 +1883,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -1891,12 +1892,13 @@ packages:
source: hosted
version: "0.9.0-dev.6"
super_native_extensions:
dependency: transitive
dependency: "direct overridden"
description:
name: super_native_extensions
sha256: "09ccc40c475e6f91770eaeb2553bf4803812d7beadc3759aa57d643370619c86"
url: "https://pub.dev"
source: hosted
path: super_native_extensions
ref: "refs/pull/525/head"
resolved-ref: d3020a8c5acd8555707b3b6477fd744d09f3e22f
url: "https://github.com/superlistapp/super_native_extensions.git"
source: git
version: "0.9.0-dev.6"
super_sliver_list:
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
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 3.0.0+95
version: 3.0.0+96
environment:
sdk: ^3.7.2
@@ -100,9 +100,9 @@ dependencies:
photo_view: ^0.15.0
dismissible_page: ^1.0.2
super_sliver_list: ^0.4.1
super_clipboard: ^0.9.0-dev.6
flutter_webrtc: ^0.14.1
livekit_client: ^2.4.7
pasteboard: ^0.4.0
dev_dependencies:
flutter_test:
@@ -130,6 +130,17 @@ dependency_overrides:
analyzer_plugin: 0.12.0
# https://github.com/dart-lang/sdk/issues/60784#issuecomment-2906872272
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
# 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
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
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="The Solar Network, an open-source social network.">
<meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta
name="description"
content="The Solar Network, an open-source social network."
/>
<!-- iOS meta tags & icons -->
<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-title" content="Solar Network">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<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-title" content="Solar Network" />
<link rel="apple-touch-icon" href="icons/Icon-192.png" />
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png">
<link rel="icon" type="image/png" href="favicon.png" />
<title>Solar Network</title>
<link rel="manifest" href="manifest.json">
<link rel="manifest" href="manifest.json" />
<style>
@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..900;1,300..900&display=swap");
html,
body {
font-family: "Rubik", sans-serif;
@@ -77,7 +82,7 @@
}
.swal-button--confirm {
background-color: #6750A4;
background-color: #6750a4;
color: #ffffff;
}
@@ -87,7 +92,7 @@
.swal-button--cancel {
background-color: transparent;
color: #6750A4;
color: #6750a4;
}
.swal-button--cancel:hover {
@@ -95,33 +100,33 @@
}
.swal-icon {
border-color: #6750A4;
border-color: #6750a4;
margin: 12px auto;
}
.swal-icon--success__line {
background-color: #6750A4;
background-color: #6750a4;
}
.swal-icon--success__ring {
border-color: #6750A4;
border-color: #6750a4;
}
.swal-icon--warning {
border-color: #F2B824;
border-color: #f2b824;
}
.swal-icon--warning__body,
.swal-icon--warning__body,
.swal-icon--warning__dot {
background-color: #F2B824;
background-color: #f2b824;
}
.swal-icon--error {
border-color: #DC362E;
border-color: #dc362e;
}
.swal-icon--error__line {
background-color: #DC362E;
background-color: #dc362e;
}
.swal-footer {
@@ -130,92 +135,144 @@
border: none;
text-align: right;
}
</style>
<style id="splash-screen-style">
html {
height: 100%
}
html {
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 {
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>
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 {
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>
<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 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 src="flutter_bootstrap.js" async=""></script>
<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
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>
document.oncontextmenu = (evt) => evt.preventDefault();
</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 <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <pasteboard/pasteboard_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
@@ -48,6 +49,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
MediaKitVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
PasteboardPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PasteboardPlugin"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(

View File

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