From 07650f4643ce12526bf72155887a37368a49b0c6 Mon Sep 17 00:00:00 2001 From: Shlomo <78599753+ShlomoCode@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:14:47 +0300 Subject: [PATCH] feat: improve remaining time display during file transfer (#2765) --- app/assets/i18n/en.json | 8 +++ app/lib/gen/strings.g.dart | 2 +- app/lib/gen/strings_en.g.dart | 18 ++++++ app/lib/util/file_speed_helper.dart | 39 ++++++++++--- .../unit/util/file_speed_helper_test.dart | 57 +++++++++++++++++++ 5 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 app/test/unit/util/file_speed_helper_test.dart diff --git a/app/assets/i18n/en.json b/app/assets/i18n/en.json index f7f10bdc..d1f929f9 100644 --- a/app/assets/i18n/en.json +++ b/app/assets/i18n/en.json @@ -239,6 +239,14 @@ "count": "Files: {curr} / {n}", "size": "Size: {curr} / {n}", "speed": "Speed: {speed}/s" + }, + "remainingTime": { + "seconds": "{n}:{ss}", + "minutes": "{n}:{ss}", + "hours": "{h}h {m}m", + "days": "{d}d {h}h {m}m", + "@hours": "Use 'h' for hours abbreviation and 'm' for minutes", + "@days": "Use 'd' for days, 'h' for hours, and 'm' for minutes" } }, "webSharePage": { diff --git a/app/lib/gen/strings.g.dart b/app/lib/gen/strings.g.dart index 523bdade..8ec8eb05 100644 --- a/app/lib/gen/strings.g.dart +++ b/app/lib/gen/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 53 -/// Strings: 16908 (319 per locale) +/// Strings: 16912 (319 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/app/lib/gen/strings_en.g.dart b/app/lib/gen/strings_en.g.dart index a5a8c6bc..299c6617 100644 --- a/app/lib/gen/strings_en.g.dart +++ b/app/lib/gen/strings_en.g.dart @@ -281,6 +281,7 @@ class TranslationsProgressPageEn { String get titleReceiving => 'Receiving files'; String get savedToGallery => 'Saved in Photos'; late final TranslationsProgressPageTotalEn total = TranslationsProgressPageTotalEn.internal(_root); + late final TranslationsProgressPageRemainingTimeEn remainingTime = TranslationsProgressPageRemainingTimeEn.internal(_root); } // Path: webSharePage @@ -763,6 +764,23 @@ class TranslationsProgressPageTotalEn { String speed({required Object speed}) => 'Speed: ${speed}/s'; } +// Path: progressPage.remainingTime +class TranslationsProgressPageRemainingTimeEn { + TranslationsProgressPageRemainingTimeEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String seconds({required Object n, required Object ss}) => '${n}:${ss}'; + String minutes({required Object n, required Object ss}) => '${n}:${ss}'; + + /// Use 'h' for hours abbreviation and 'm' for minutes + String hours({required Object h, required Object m}) => '${h}h ${m}m'; + + /// Use 'd' for days, 'h' for hours, and 'm' for minutes + String days({required Object d, required Object h, required Object m}) => '${d}d ${h}h ${m}m'; +} + // Path: dialogs.addFile class TranslationsDialogsAddFileEn { TranslationsDialogsAddFileEn.internal(this._root); diff --git a/app/lib/util/file_speed_helper.dart b/app/lib/util/file_speed_helper.dart index fa0203b8..ad9544e0 100644 --- a/app/lib/util/file_speed_helper.dart +++ b/app/lib/util/file_speed_helper.dart @@ -1,25 +1,48 @@ -/// Returns bytes per second +import 'package:localsend_app/gen/strings.g.dart'; + +const _millisecondsPerSecond = 1000; +const _secondsPerMinute = 60; +const _secondsPerHour = 3600; +const _secondsPerDay = 86400; + int getFileSpeed({ required int start, required int end, required int bytes, }) { final deltaTime = end - start; - return (1000 * bytes) ~/ deltaTime; // multiply by 1000 to convert millis to seconds + return (_millisecondsPerSecond * bytes) ~/ deltaTime; } -/// Returns remaining time in m:ss String getRemainingTime({ required int bytesPerSeconds, required int remainingBytes, }) { - final totalSeconds = _getRemainingTime(bytesPerSeconds: bytesPerSeconds, remainingBytes: remainingBytes); - final minutes = totalSeconds ~/ 60; - final seconds = totalSeconds % 60; - return '$minutes:${seconds.toString().padLeft(2, '0')}'; + if (bytesPerSeconds == 0) { + return remainingBytes == 0 ? t.progressPage.remainingTime.seconds(n: 0, ss: '00') : '∞'; + } + + final remainingTimeInSeconds = _getRemainingTime(bytesPerSeconds: bytesPerSeconds, remainingBytes: remainingBytes); + + if (remainingTimeInSeconds < _secondsPerMinute) { + return t.progressPage.remainingTime.seconds(n: 0, ss: remainingTimeInSeconds.toString().padLeft(2, '0')); + } else if (remainingTimeInSeconds < _secondsPerHour) { + final minutes = remainingTimeInSeconds ~/ _secondsPerMinute; + final seconds = remainingTimeInSeconds % _secondsPerMinute; + return t.progressPage.remainingTime.minutes(n: minutes, ss: seconds.toString().padLeft(2, '0')); + } else if (remainingTimeInSeconds < _secondsPerDay) { + final hours = remainingTimeInSeconds ~/ _secondsPerHour; + final minutes = (remainingTimeInSeconds % _secondsPerHour) ~/ _secondsPerMinute; + return t.progressPage.remainingTime.hours(h: hours, m: minutes); + } else { + final days = remainingTimeInSeconds ~/ _secondsPerDay; + final remainingAfterDays = remainingTimeInSeconds % _secondsPerDay; + final hours = remainingAfterDays ~/ _secondsPerHour; + final minutes = (remainingAfterDays % _secondsPerHour) ~/ _secondsPerMinute; + return t.progressPage.remainingTime.days(d: days, h: hours, m: minutes); + } } -/// Returns remaining time in seconds int _getRemainingTime({ required int bytesPerSeconds, required int remainingBytes, diff --git a/app/test/unit/util/file_speed_helper_test.dart b/app/test/unit/util/file_speed_helper_test.dart new file mode 100644 index 00000000..09b5fc41 --- /dev/null +++ b/app/test/unit/util/file_speed_helper_test.dart @@ -0,0 +1,57 @@ +import 'package:localsend_app/util/file_speed_helper.dart'; +import 'package:test/test.dart'; + +void main() { + group('getRemainingTime', () { + test('shows seconds for duration less than 1 minute', () { + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 30000), '0:30'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 45000), '0:45'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 5000), '0:05'); + }); + + test('shows minutes and seconds for duration less than 1 hour', () { + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 90000), '1:30'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 600000), '10:00'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 3540000), '59:00'); + }); + + test('shows hours and minutes for duration 1 hour or more but less than 1 day', () { + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 3600000), '1h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 3660000), '1h 1m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 7200000), '2h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 12000000), '3h 20m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 86340000), '23h 59m'); + }); + + test('shows days, hours and minutes for duration 1 day or more', () { + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 86400000), '1d 0h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 90000000), '1d 1h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 93660000), '1d 2h 1m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 172800000), '2d 0h 0m'); + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 183780000), '2d 3h 3m'); + }); + + test('handles zero remaining bytes', () { + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 0), '0:00'); + expect(getRemainingTime(bytesPerSeconds: 0, remainingBytes: 0), '0:00'); + }); + + test('handles zero speed with remaining bytes', () { + expect(getRemainingTime(bytesPerSeconds: 0, remainingBytes: 1000), '∞'); + expect(getRemainingTime(bytesPerSeconds: 0, remainingBytes: 1000000), '∞'); + }); + + test('handles edge cases', () { + expect(getRemainingTime(bytesPerSeconds: 1000, remainingBytes: 1000), '0:01'); + expect(getRemainingTime(bytesPerSeconds: 1, remainingBytes: 1), '0:01'); + }); + }); + + group('getFileSpeed', () { + test('calculates bytes per second correctly', () { + expect(getFileSpeed(start: 0, end: 1000, bytes: 1000), 1000); + expect(getFileSpeed(start: 0, end: 2000, bytes: 4000), 2000); + expect(getFileSpeed(start: 1000, end: 3000, bytes: 10000), 5000); + }); + }); +}