Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/core/app/app_version.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const appVersionName = '1.0.11';
const appVersionCode = 15;
const appVersionName = '1.0.13';
const appVersionCode = 17;
const appVersion = '$appVersionName+$appVersionCode';

const ctcpVersionReply = 'AndroidIRCX Flutter v$appVersion';
13 changes: 13 additions & 0 deletions lib/core/models/app_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ enum MessageDensity { compact, comfortable, relaxed }

enum NickColorMode { none, soft, vivid }

enum MediaAutoDownloadMode { never, wifiOnly, always }

class AppSettings {
const AppSettings({
this.showRawEvents = true,
Expand All @@ -20,6 +22,7 @@ class AppSettings {
this.showAttachmentPreviews = true,
this.dccDownloadDirectoryPath = '',
this.mediaDownloadDirectoryPath = '',
this.mediaAutoDownloadMode = MediaAutoDownloadMode.never,
this.themePreset = AppThemePreset.light,
this.customThemeJson = '',
this.messageFontScale = 1.0,
Expand Down Expand Up @@ -61,6 +64,7 @@ class AppSettings {
final bool showAttachmentPreviews;
final String dccDownloadDirectoryPath;
final String mediaDownloadDirectoryPath;
final MediaAutoDownloadMode mediaAutoDownloadMode;
final AppThemePreset themePreset;
final String customThemeJson;
final double messageFontScale;
Expand Down Expand Up @@ -135,6 +139,7 @@ class AppSettings {
bool? showAttachmentPreviews,
String? dccDownloadDirectoryPath,
String? mediaDownloadDirectoryPath,
MediaAutoDownloadMode? mediaAutoDownloadMode,
AppThemePreset? themePreset,
String? customThemeJson,
double? messageFontScale,
Expand Down Expand Up @@ -180,6 +185,8 @@ class AppSettings {
dccDownloadDirectoryPath ?? this.dccDownloadDirectoryPath,
mediaDownloadDirectoryPath:
mediaDownloadDirectoryPath ?? this.mediaDownloadDirectoryPath,
mediaAutoDownloadMode:
mediaAutoDownloadMode ?? this.mediaAutoDownloadMode,
themePreset: themePreset ?? this.themePreset,
customThemeJson: customThemeJson ?? this.customThemeJson,
messageFontScale: _clampFontScale(
Expand Down Expand Up @@ -229,6 +236,7 @@ class AppSettings {
'showAttachmentPreviews': showAttachmentPreviews,
'dccDownloadDirectoryPath': dccDownloadDirectoryPath,
'mediaDownloadDirectoryPath': mediaDownloadDirectoryPath,
'mediaAutoDownloadMode': mediaAutoDownloadMode.name,
'themePreset': themePreset.name,
'customThemeJson': customThemeJson,
'messageFontScale': messageFontScale,
Expand Down Expand Up @@ -279,6 +287,11 @@ class AppSettings {
(json['dccDownloadDirectoryPath'] as String?)?.trim() ?? '',
mediaDownloadDirectoryPath:
(json['mediaDownloadDirectoryPath'] as String?)?.trim() ?? '',
mediaAutoDownloadMode: _enumByName(
MediaAutoDownloadMode.values,
json['mediaAutoDownloadMode'],
MediaAutoDownloadMode.never,
),
themePreset: _enumByName(
AppThemePreset.values,
json['themePreset'],
Expand Down
107 changes: 103 additions & 4 deletions lib/features/chat/presentation/chat_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import 'package:androidircx/irc/parser/irc_formatter.dart';
import 'package:androidircx/irc/parser/message_content_parser.dart';
import 'package:androidircx/features/settings/presentation/settings_screen.dart';
import 'package:androidircx/media/services/link_preview_service.dart';
import 'package:androidircx/media/services/media_auto_download_policy.dart';
import 'package:androidircx/media/services/media_download_service.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
Expand All @@ -42,6 +43,7 @@ class ChatScreen extends StatefulWidget {
required this.controller,
this.filePicker,
this.mediaDownloadService,
this.mediaAutoDownloadPolicy,
this.sessionRegistry,
this.networkController,
this.onSwitchNetwork,
Expand All @@ -53,6 +55,7 @@ class ChatScreen extends StatefulWidget {
final ChatSessionController controller;
final DccFilePicker? filePicker;
final MediaDownloadService? mediaDownloadService;
final MediaAutoDownloadPolicy? mediaAutoDownloadPolicy;

/// Local per-channel notes store; defaults to a shared-preferences backed
/// instance. Injectable for tests.
Expand Down Expand Up @@ -92,12 +95,19 @@ class _ChatScreenState extends State<ChatScreen> {
String _nickSearchQuery = '';
IrcMessage? _pendingReplyMessage;
bool _connectedBannerDismissed = false;
final Set<String> _autoDownloadSeenAttachmentKeys = <String>{};
final Set<String> _autoDownloadInFlightAttachmentKeys = <String>{};

ChatSessionController get _controller => widget.controller;
DccFilePicker get _filePicker =>
widget.filePicker ?? const MethodChannelDccFilePicker();
MediaDownloadService get _mediaDownloadService =>
widget.mediaDownloadService ?? createMediaDownloadService();
MediaAutoDownloadPolicy? _defaultMediaAutoDownloadPolicy;
MediaAutoDownloadPolicy get _mediaAutoDownloadPolicy =>
widget.mediaAutoDownloadPolicy ??
(_defaultMediaAutoDownloadPolicy ??=
ConnectivityMediaAutoDownloadPolicy());
ChannelNotesRepository? _defaultChannelNotes;
ChannelNotesRepository get _channelNotesRepository =>
widget.channelNotesRepository ??
Expand All @@ -110,30 +120,39 @@ class _ChatScreenState extends State<ChatScreen> {
@override
void initState() {
super.initState();
_controller.addListener(_syncConnectionBannerDismissal);
_rememberExistingAutoDownloadAttachments();
_controller.addListener(_handleControllerChanged);
_controller.start();
}

@override
void didUpdateWidget(covariant ChatScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (!identical(oldWidget.controller, widget.controller)) {
oldWidget.controller.removeListener(_syncConnectionBannerDismissal);
oldWidget.controller.removeListener(_handleControllerChanged);
_connectedBannerDismissed = false;
_controller.addListener(_syncConnectionBannerDismissal);
_autoDownloadSeenAttachmentKeys.clear();
_autoDownloadInFlightAttachmentKeys.clear();
_rememberExistingAutoDownloadAttachments();
_controller.addListener(_handleControllerChanged);
_controller.start();
}
}

@override
void dispose() {
_controller.removeListener(_syncConnectionBannerDismissal);
_controller.removeListener(_handleControllerChanged);
_composerController.dispose();
_messageSearchController.dispose();
_nickSearchController.dispose();
super.dispose();
}

void _handleControllerChanged() {
_syncConnectionBannerDismissal();
_queueMediaAutoDownloads();
}

void _syncConnectionBannerDismissal() {
final snapshot = _controller.connection;
final stableConnected =
Expand All @@ -145,6 +164,74 @@ class _ChatScreenState extends State<ChatScreen> {
setState(() => _connectedBannerDismissed = false);
}

void _rememberExistingAutoDownloadAttachments() {
for (final tab in _controller.tabs) {
for (final message in _controller.messagesForTab(tab.id)) {
for (final attachment in message.attachments) {
if (_canDownloadAttachment(attachment)) {
_autoDownloadSeenAttachmentKeys.add(
_autoDownloadAttachmentKey(message, attachment),
);
}
}
}
}
}

void _queueMediaAutoDownloads() {
final mode = _controller.settings.mediaAutoDownloadMode;
if (mode == MediaAutoDownloadMode.never) {
_rememberExistingAutoDownloadAttachments();
return;
}

for (final tab in _controller.tabs) {
for (final message in _controller.messagesForTab(tab.id)) {
for (final attachment in message.attachments) {
if (!_canDownloadAttachment(attachment)) {
continue;
}
final key = _autoDownloadAttachmentKey(message, attachment);
if (!_autoDownloadSeenAttachmentKeys.add(key)) {
continue;
}
if (message.isOwn || message.isPlayback) {
continue;
}
if (!_autoDownloadInFlightAttachmentKeys.add(key)) {
continue;
}
unawaited(_autoDownloadAttachment(key, attachment, mode));
}
}
}
}

Future<void> _autoDownloadAttachment(
String key,
IrcMessageAttachment attachment,
MediaAutoDownloadMode mode,
) async {
try {
if (!await _mediaAutoDownloadPolicy.canAutoDownload(mode)) {
return;
}
final url = attachment.uri;
if (url == null || url.trim().isEmpty) {
return;
}
await _mediaDownloadService.download(
url,
directoryPath: _controller.settings.mediaDownloadDirectoryPath,
);
} catch (_) {
// Auto-download must never interrupt chat; manual download remains
// available from the attachment card when a background attempt fails.
} finally {
_autoDownloadInFlightAttachmentKeys.remove(key);
}
}

@override
Widget build(BuildContext context) {
return CallbackShortcuts(
Expand Down Expand Up @@ -4196,6 +4283,18 @@ bool _canDownloadAttachment(IrcMessageAttachment attachment) {
};
}

String _autoDownloadAttachmentKey(
IrcMessage message,
IrcMessageAttachment attachment,
) {
return [
message.networkId ?? '',
message.tabId,
message.id,
attachment.uri?.trim() ?? '',
].join('\u001f');
}

class _ConnectionBanner extends StatelessWidget {
const _ConnectionBanner({
required this.controller,
Expand Down
Loading
Loading