Compare commits
3 Commits
e922971a5e
...
113309257e
Author | SHA1 | Date | |
---|---|---|---|
113309257e | |||
b95a8b2ed2 | |||
9d5b71bead |
@@ -131,7 +131,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final messages = ref.watch(messagesNotifierProvider(id));
|
final messages = ref.watch(messagesNotifierProvider(id));
|
||||||
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
||||||
final chatSubscribe = ref.watch(chatSubscribeNotifierProvider(id));
|
|
||||||
final chatSubscribeNotifier = ref.read(
|
final chatSubscribeNotifier = ref.read(
|
||||||
chatSubscribeNotifierProvider(id).notifier,
|
chatSubscribeNotifierProvider(id).notifier,
|
||||||
);
|
);
|
||||||
@@ -613,77 +612,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
(room) => Column(
|
(room) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 150),
|
|
||||||
switchInCurve: Curves.fastEaseInToSlowEaseOut,
|
|
||||||
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
|
||||||
transitionBuilder: (
|
|
||||||
Widget child,
|
|
||||||
Animation<double> animation,
|
|
||||||
) {
|
|
||||||
return SlideTransition(
|
|
||||||
position: Tween<Offset>(
|
|
||||||
begin: const Offset(0, -0.3),
|
|
||||||
end: Offset.zero,
|
|
||||||
).animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: animation,
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SizeTransition(
|
|
||||||
sizeFactor: animation,
|
|
||||||
axisAlignment: -1.0,
|
|
||||||
child: FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child:
|
|
||||||
chatSubscribe.isNotEmpty
|
|
||||||
? Container(
|
|
||||||
key: const ValueKey('typing-indicator'),
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Symbols.more_horiz,
|
|
||||||
size: 16,
|
|
||||||
).padding(horizontal: 8),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'typingHint'.plural(
|
|
||||||
chatSubscribe.length,
|
|
||||||
args: [
|
|
||||||
chatSubscribe
|
|
||||||
.map(
|
|
||||||
(x) =>
|
|
||||||
x.nick ??
|
|
||||||
x.account.nick,
|
|
||||||
)
|
|
||||||
.join(', '),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
style:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(
|
|
||||||
key: ValueKey('typing-indicator-none'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ChatInput(
|
ChatInput(
|
||||||
messageController: messageController,
|
messageController: messageController,
|
||||||
chatRoom: room!,
|
chatRoom: room!,
|
||||||
@@ -748,6 +676,30 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
top: 0,
|
top: 0,
|
||||||
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
||||||
),
|
),
|
||||||
|
if (isSyncing)
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.8),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('Syncing...', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -233,8 +233,208 @@ class UpdateService {
|
|||||||
return 'https://fs.solsynth.dev/d/official/solian/build-output-windows-installer.zip';
|
return 'https://fs.solsynth.dev/d/official/solian/build-output-windows-installer.zip';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs automatic Windows update: download, extract, and install
|
||||||
|
Future<void> performAutomaticWindowsUpdate(
|
||||||
|
BuildContext context,
|
||||||
|
String url,
|
||||||
|
) async {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => _WindowsUpdateDialog(
|
||||||
|
updateUrl: url,
|
||||||
|
onComplete: () {
|
||||||
|
// Close the update sheet
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the latest release info from GitHub.
|
||||||
|
/// Public so other screens (e.g., About) can manually trigger update checks.
|
||||||
|
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
||||||
|
final apiEndpoint =
|
||||||
|
useProxy
|
||||||
|
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
|
||||||
|
: _releasesLatestApi;
|
||||||
|
|
||||||
|
log(
|
||||||
|
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
|
||||||
|
);
|
||||||
|
final resp = await _dio.get(apiEndpoint);
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
log(
|
||||||
|
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final data = resp.data as Map<String, dynamic>;
|
||||||
|
log('[Update] Successfully fetched release data.');
|
||||||
|
|
||||||
|
final tagName = (data['tag_name'] ?? '').toString();
|
||||||
|
final name = (data['name'] ?? tagName).toString();
|
||||||
|
final body = (data['body'] ?? '').toString();
|
||||||
|
final htmlUrl = (data['html_url'] ?? '').toString();
|
||||||
|
final createdAtStr = (data['created_at'] ?? '').toString();
|
||||||
|
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
||||||
|
final assetsData =
|
||||||
|
(data['assets'] as List<dynamic>?)
|
||||||
|
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (tagName.isEmpty || htmlUrl.isEmpty) {
|
||||||
|
log(
|
||||||
|
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
|
||||||
|
return GithubReleaseInfo(
|
||||||
|
tagName: tagName,
|
||||||
|
name: name,
|
||||||
|
body: body,
|
||||||
|
htmlUrl: htmlUrl,
|
||||||
|
createdAt: createdAt,
|
||||||
|
assets: assetsData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WindowsUpdateDialog extends StatefulWidget {
|
||||||
|
const _WindowsUpdateDialog({
|
||||||
|
required this.updateUrl,
|
||||||
|
required this.onComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String updateUrl;
|
||||||
|
final VoidCallback onComplete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_WindowsUpdateDialog> createState() => _WindowsUpdateDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||||
|
final ValueNotifier<double?> progressNotifier = ValueNotifier<double?>(null);
|
||||||
|
final ValueNotifier<String> messageNotifier = ValueNotifier<String>('Downloading installer...');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_startUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startUpdate() async {
|
||||||
|
try {
|
||||||
|
// Step 1: Download
|
||||||
|
final zipPath = await _downloadWindowsInstaller(
|
||||||
|
widget.updateUrl,
|
||||||
|
onProgress: (received, total) {
|
||||||
|
if (total == -1) {
|
||||||
|
progressNotifier.value = null;
|
||||||
|
} else {
|
||||||
|
progressNotifier.value = received / total;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (zipPath == null) {
|
||||||
|
_showError('Failed to download installer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Extract
|
||||||
|
messageNotifier.value = 'Extracting installer...';
|
||||||
|
progressNotifier.value = null; // Indeterminate for extraction
|
||||||
|
|
||||||
|
final extractDir = await _extractWindowsInstaller(zipPath);
|
||||||
|
if (extractDir == null) {
|
||||||
|
_showError('Failed to extract installer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Run installer
|
||||||
|
messageNotifier.value = 'Running installer...';
|
||||||
|
|
||||||
|
final success = await _runWindowsInstaller(extractDir);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
messageNotifier.value = 'Update Complete';
|
||||||
|
progressNotifier.value = 1.0;
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
widget.onComplete();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showError('Failed to run installer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
try {
|
||||||
|
await File(zipPath).delete();
|
||||||
|
await Directory(extractDir).delete(recursive: true);
|
||||||
|
} catch (e) {
|
||||||
|
log('[Update] Error cleaning up temporary files: $e');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_showError('Update failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Update Failed'),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Installing Update'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ValueListenableBuilder<double?>(
|
||||||
|
valueListenable: progressNotifier,
|
||||||
|
builder: (context, progress, child) {
|
||||||
|
return LinearProgressIndicator(value: progress);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ValueListenableBuilder<String>(
|
||||||
|
valueListenable: messageNotifier,
|
||||||
|
builder: (context, message, child) {
|
||||||
|
return Text(message);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Downloads the Windows installer ZIP file
|
/// Downloads the Windows installer ZIP file
|
||||||
Future<String?> _downloadWindowsInstaller(String url) async {
|
Future<String?> _downloadWindowsInstaller(
|
||||||
|
String url, {
|
||||||
|
void Function(int received, int total)? onProgress,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
log('[Update] Starting Windows installer download from: $url');
|
log('[Update] Starting Windows installer download from: $url');
|
||||||
|
|
||||||
@@ -243,7 +443,7 @@ class UpdateService {
|
|||||||
'solian-installer-${DateTime.now().millisecondsSinceEpoch}.zip';
|
'solian-installer-${DateTime.now().millisecondsSinceEpoch}.zip';
|
||||||
final filePath = path.join(tempDir.path, fileName);
|
final filePath = path.join(tempDir.path, fileName);
|
||||||
|
|
||||||
final response = await _dio.download(
|
final response = await Dio().download(
|
||||||
url,
|
url,
|
||||||
filePath,
|
filePath,
|
||||||
onReceiveProgress: (received, total) {
|
onReceiveProgress: (received, total) {
|
||||||
@@ -252,6 +452,7 @@ class UpdateService {
|
|||||||
'[Update] Download progress: ${(received / total * 100).toStringAsFixed(1)}%',
|
'[Update] Download progress: ${(received / total * 100).toStringAsFixed(1)}%',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
onProgress?.call(received, total);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -311,13 +512,20 @@ class UpdateService {
|
|||||||
try {
|
try {
|
||||||
log('[Update] Running Windows installer from: $extractDir');
|
log('[Update] Running Windows installer from: $extractDir');
|
||||||
|
|
||||||
final setupExePath = path.join(extractDir, 'setup.exe');
|
final dir = Directory(extractDir);
|
||||||
|
final exeFiles = dir
|
||||||
|
.listSync()
|
||||||
|
.where((f) => f is File && f.path.endsWith('.exe'))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (!await File(setupExePath).exists()) {
|
if (exeFiles.isEmpty) {
|
||||||
log('[Update] setup.exe not found in extracted directory');
|
log('[Update] No .exe file found in extracted directory');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final setupExePath = exeFiles.first.path;
|
||||||
|
log('[Update] Found installer executable: $setupExePath');
|
||||||
|
|
||||||
final shell = Shell();
|
final shell = Shell();
|
||||||
final results = await shell.run(setupExePath);
|
final results = await shell.run(setupExePath);
|
||||||
final result = results.first;
|
final result = results.first;
|
||||||
@@ -338,202 +546,6 @@ class UpdateService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs automatic Windows update: download, extract, and install
|
|
||||||
Future<void> _performAutomaticWindowsUpdate(
|
|
||||||
BuildContext context,
|
|
||||||
String url,
|
|
||||||
) async {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
// Show progress dialog
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder:
|
|
||||||
(context) => const AlertDialog(
|
|
||||||
title: Text('Installing Update'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Downloading installer...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: Download
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop(); // Close progress dialog
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder:
|
|
||||||
(context) => const AlertDialog(
|
|
||||||
title: Text('Installing Update'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Extracting installer...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final zipPath = await _downloadWindowsInstaller(url);
|
|
||||||
if (zipPath == null) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
_showErrorDialog(context, 'Failed to download installer');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Extract
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop(); // Close progress dialog
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder:
|
|
||||||
(context) => const AlertDialog(
|
|
||||||
title: Text('Installing Update'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Running installer...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final extractDir = await _extractWindowsInstaller(zipPath);
|
|
||||||
if (extractDir == null) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
_showErrorDialog(context, 'Failed to extract installer');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Run installer
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop(); // Close progress dialog
|
|
||||||
|
|
||||||
final success = await _runWindowsInstaller(extractDir);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => AlertDialog(
|
|
||||||
title: const Text('Update Complete'),
|
|
||||||
content: const Text(
|
|
||||||
'The application has been updated successfully. Please restart the application.',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
// Close the update sheet
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_showErrorDialog(context, 'Failed to run installer');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
try {
|
|
||||||
await File(zipPath).delete();
|
|
||||||
await Directory(extractDir).delete(recursive: true);
|
|
||||||
} catch (e) {
|
|
||||||
log('[Update] Error cleaning up temporary files: $e');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop(); // Close any open dialogs
|
|
||||||
_showErrorDialog(context, 'Update failed: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showErrorDialog(BuildContext context, String message) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => AlertDialog(
|
|
||||||
title: const Text('Update Failed'),
|
|
||||||
content: Text(message),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch the latest release info from GitHub.
|
|
||||||
/// Public so other screens (e.g., About) can manually trigger update checks.
|
|
||||||
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
|
||||||
final apiEndpoint =
|
|
||||||
useProxy
|
|
||||||
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
|
|
||||||
: _releasesLatestApi;
|
|
||||||
|
|
||||||
log(
|
|
||||||
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
|
|
||||||
);
|
|
||||||
final resp = await _dio.get(apiEndpoint);
|
|
||||||
if (resp.statusCode != 200) {
|
|
||||||
log(
|
|
||||||
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final data = resp.data as Map<String, dynamic>;
|
|
||||||
log('[Update] Successfully fetched release data.');
|
|
||||||
|
|
||||||
final tagName = (data['tag_name'] ?? '').toString();
|
|
||||||
final name = (data['name'] ?? tagName).toString();
|
|
||||||
final body = (data['body'] ?? '').toString();
|
|
||||||
final htmlUrl = (data['html_url'] ?? '').toString();
|
|
||||||
final createdAtStr = (data['created_at'] ?? '').toString();
|
|
||||||
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
|
||||||
final assetsData =
|
|
||||||
(data['assets'] as List<dynamic>?)
|
|
||||||
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList() ??
|
|
||||||
[];
|
|
||||||
|
|
||||||
if (tagName.isEmpty || htmlUrl.isEmpty) {
|
|
||||||
log(
|
|
||||||
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
|
|
||||||
return GithubReleaseInfo(
|
|
||||||
tagName: tagName,
|
|
||||||
name: name,
|
|
||||||
body: body,
|
|
||||||
htmlUrl: htmlUrl,
|
|
||||||
createdAt: createdAt,
|
|
||||||
assets: assetsData,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UpdateSheet extends StatefulWidget {
|
class _UpdateSheet extends StatefulWidget {
|
||||||
@@ -655,7 +667,7 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
|||||||
final updateService = UpdateService(
|
final updateService = UpdateService(
|
||||||
useProxy: widget.useProxy,
|
useProxy: widget.useProxy,
|
||||||
);
|
);
|
||||||
updateService._performAutomaticWindowsUpdate(
|
updateService.performAutomaticWindowsUpdate(
|
||||||
context,
|
context,
|
||||||
widget.windowsUpdateUrl!,
|
widget.windowsUpdateUrl!,
|
||||||
);
|
);
|
||||||
|
@@ -15,6 +15,7 @@ import "package:pasteboard/pasteboard.dart";
|
|||||||
import "package:styled_widget/styled_widget.dart";
|
import "package:styled_widget/styled_widget.dart";
|
||||||
import "package:material_symbols_icons/symbols.dart";
|
import "package:material_symbols_icons/symbols.dart";
|
||||||
import "package:island/widgets/stickers/picker.dart";
|
import "package:island/widgets/stickers/picker.dart";
|
||||||
|
import "package:island/pods/chat/chat_subscribe.dart";
|
||||||
|
|
||||||
class ChatInput extends HookConsumerWidget {
|
class ChatInput extends HookConsumerWidget {
|
||||||
final TextEditingController messageController;
|
final TextEditingController messageController;
|
||||||
@@ -53,6 +54,7 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final inputFocusNode = useFocusNode();
|
final inputFocusNode = useFocusNode();
|
||||||
|
final chatSubscribe = ref.watch(chatSubscribeNotifierProvider(chatRoom.id));
|
||||||
|
|
||||||
void send() {
|
void send() {
|
||||||
onSend.call();
|
onSend.call();
|
||||||
@@ -125,6 +127,77 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
switchInCurve: Curves.fastEaseInToSlowEaseOut,
|
||||||
|
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
||||||
|
transitionBuilder: (
|
||||||
|
Widget child,
|
||||||
|
Animation<double> animation,
|
||||||
|
) {
|
||||||
|
return SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, -0.3),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
axisAlignment: -1.0,
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
chatSubscribe.isNotEmpty
|
||||||
|
? Container(
|
||||||
|
key: const ValueKey('typing-indicator'),
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Symbols.more_horiz,
|
||||||
|
size: 16,
|
||||||
|
).padding(horizontal: 8),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'typingHint'.plural(
|
||||||
|
chatSubscribe.length,
|
||||||
|
args: [
|
||||||
|
chatSubscribe
|
||||||
|
.map(
|
||||||
|
(x) =>
|
||||||
|
x.nick ??
|
||||||
|
x.account.nick,
|
||||||
|
)
|
||||||
|
.join(', '),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(
|
||||||
|
key: ValueKey('typing-indicator-none'),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (attachments.isNotEmpty)
|
if (attachments.isNotEmpty)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 180,
|
height: 180,
|
||||||
|
Reference in New Issue
Block a user