Line data Source code
1 : import 'dart:convert';
2 :
3 : import 'package:cwtch/main.dart';
4 : import 'package:cwtch/models/contact.dart';
5 : import 'package:cwtch/models/profile.dart';
6 : import 'package:cwtch/settings.dart';
7 : import 'package:flutter/material.dart';
8 : import 'package:provider/provider.dart';
9 : import 'package:flutter_gen/gen_l10n/app_localizations.dart';
10 : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
11 :
12 : import '../cwtch_icons_icons.dart';
13 :
14 : class FileSharingView extends StatefulWidget {
15 0 : @override
16 0 : _FileSharingViewState createState() => _FileSharingViewState();
17 : }
18 :
19 : class _FileSharingViewState extends State<FileSharingView> {
20 0 : @override
21 : Widget build(BuildContext context) {
22 0 : var handle = Provider.of<ContactInfoState>(context).nickname;
23 0 : if (handle.isEmpty) {
24 0 : handle = Provider.of<ContactInfoState>(context).onion;
25 : }
26 :
27 0 : var profileHandle = Provider.of<ProfileInfoState>(context).onion;
28 :
29 0 : return Scaffold(
30 0 : appBar: AppBar(
31 0 : title: Text(handle + " ยป " + AppLocalizations.of(context)!.manageSharedFiles),
32 : ),
33 0 : backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor,
34 0 : body: FutureBuilder(
35 0 : future: Provider.of<FlwtchState>(context, listen: false).cwtch.GetSharedFiles(profileHandle, Provider.of<ContactInfoState>(context).identifier),
36 0 : builder: (context, snapshot) {
37 0 : if (snapshot.hasData) {
38 0 : List<dynamic> sharedFiles = jsonDecode(snapshot.data as String) ?? List<dynamic>.empty();
39 : // Stabilize sort when files are assigned identical DateShared.
40 0 : sharedFiles.sort((a, b) {
41 0 : final cmp = a["DateShared"].toString().compareTo(b["DateShared"].toString());
42 0 : return cmp == 0 ? a["FileKey"].compareTo(b["FileKey"]) : cmp;
43 : });
44 0 : sharedFiles = injectTimeHeaders(AppLocalizations.of(context), sharedFiles, DateTime.now());
45 :
46 0 : var fileList = ScrollablePositionedList.separated(
47 0 : itemScrollController: ItemScrollController(),
48 0 : itemCount: sharedFiles.length,
49 : shrinkWrap: true,
50 : reverse: true,
51 0 : physics: BouncingScrollPhysics(),
52 0 : semanticChildCount: sharedFiles.length,
53 0 : itemBuilder: (context, index) {
54 : // New for #589: some entries in the time-sorted list
55 : // are placeholders for relative time headers. These should
56 : // be placed in simple tiles for aesthetic pleasure.
57 : // The dict only contains TimeHeader key, and as such
58 : // the downstream logic needs to be avoided.
59 0 : if (sharedFiles[index].containsKey("TimeHeader")) {
60 0 : return ListTile(title: Text(sharedFiles[index]["TimeHeader"]), dense: true);
61 : }
62 0 : String filekey = sharedFiles[index]["FileKey"];
63 : // This makes the UI *very* slow when enabled. But can be useful for debugging
64 : // Uncomment if necessary.
65 : // EnvironmentConfig.debugLog("$sharedFiles " + sharedFiles[index].toString());
66 0 : return SwitchListTile(
67 0 : title: Text(sharedFiles[index]["Path"]),
68 0 : subtitle: Text(sharedFiles[index]["DateShared"]),
69 0 : value: sharedFiles[index]["Active"],
70 0 : activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
71 0 : inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
72 0 : secondary: Icon(CwtchIcons.attached_file_3, color: Provider.of<Settings>(context).current().mainTextColor),
73 0 : onChanged: (newValue) {
74 0 : setState(() {
75 : if (newValue) {
76 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.RestartSharing(profileHandle, filekey);
77 : } else {
78 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.StopSharing(profileHandle, filekey);
79 : }
80 : });
81 : });
82 : },
83 : // Here is seemingly approximately the location where date dividers could be injected.
84 : // See #589 fore description. Seems like this lambda could get refactored out into
85 : // a testable function that determines time in the past relative to now and emits
86 : // a localized text banner as necessary. Time and language localization creates
87 : // a possible difficulty for this one.
88 0 : separatorBuilder: (BuildContext context, int index) {
89 0 : return Divider(height: 1);
90 : },
91 : );
92 : return fileList;
93 : }
94 0 : return Container();
95 : },
96 : ),
97 : );
98 : }
99 : }
100 :
101 1 : bool isToday(DateTime inputTimestamp, DateTime inputNow) {
102 1 : final localTimestamp = inputTimestamp.toLocal();
103 1 : final localNow = inputNow.toLocal();
104 9 : return localTimestamp.day == localNow.day && localTimestamp.month == localNow.month && localTimestamp.year == localNow.year;
105 : }
106 :
107 1 : bool isYesterday(DateTime inputTimestamp, DateTime inputNow) {
108 1 : final adjustedTimestamp = inputTimestamp.add(const Duration(hours: 24));
109 1 : return isToday(adjustedTimestamp, inputNow);
110 : }
111 :
112 1 : bool isThisWeek(DateTime inputTimestamp, DateTime inputNow) {
113 : // note that dart internally measures weeks starting from Mondays,
114 : // so the idea of something being earlier this week is finicky.
115 1 : final localTimestamp = inputTimestamp.toLocal();
116 1 : final localNow = inputNow.toLocal();
117 8 : final thisWeekLb = DateTime(localNow.year, localNow.month, localNow.day).subtract(Duration(days: localNow.weekday - 1, microseconds: 1));
118 1 : final thisWeekUb = thisWeekLb.add(const Duration(days: DateTime.daysPerWeek, microseconds: 1));
119 2 : return thisWeekLb.isBefore(localTimestamp) && thisWeekUb.isAfter(localTimestamp);
120 : }
121 :
122 1 : bool isLastWeek(DateTime inputTimestamp, DateTime inputNow) {
123 : // this inherits the same week definition issues as isThisWeek.
124 3 : final adjustedTimestamp = inputTimestamp.toLocal().add(Duration(days: DateTime.daysPerWeek));
125 1 : return isThisWeek(adjustedTimestamp, inputNow);
126 : }
127 :
128 1 : bool isThisMonth(DateTime inputTimestamp, DateTime inputNow) {
129 1 : final localTimestamp = inputTimestamp.toLocal();
130 1 : final localNow = inputNow.toLocal();
131 6 : return localTimestamp.month == localNow.month && localTimestamp.year == localNow.year;
132 : }
133 :
134 1 : bool isLastMonth(DateTime inputTimestamp, DateTime inputNow) {
135 1 : final localTimestamp = inputTimestamp.toLocal();
136 1 : final localNow = inputNow.toLocal();
137 7 : return (localTimestamp.year == localNow.year && localTimestamp.month == localNow.month - 1) ||
138 8 : (localTimestamp.year == localNow.year - 1 && localTimestamp.month == DateTime.december && localNow.month == DateTime.january);
139 : }
140 :
141 1 : bool isThisYear(DateTime inputTimestamp, DateTime inputNow) {
142 1 : final localTimestamp = inputTimestamp.toLocal();
143 1 : final localNow = inputNow.toLocal();
144 3 : return localTimestamp.year == localNow.year;
145 : }
146 :
147 1 : bool isEarlierThanThisYear(DateTime inputTimestamp, DateTime inputNow) {
148 1 : final localTimestamp = inputTimestamp.toLocal();
149 1 : final localNow = inputNow.toLocal();
150 3 : return localTimestamp.year < localNow.year;
151 : }
152 :
153 : enum FileRelativeTime {
154 : Today,
155 : Yesterday,
156 : ThisWeek,
157 : LastWeek,
158 : ThisMonth,
159 : LastMonth,
160 : ThisYear,
161 : Earlier,
162 : }
163 :
164 1 : FileRelativeTime assignRelativeTime(DateTime timestamp, DateTime now) {
165 1 : if (isToday(timestamp, now)) {
166 : return FileRelativeTime.Today;
167 1 : } else if (isYesterday(timestamp, now)) {
168 : return FileRelativeTime.Yesterday;
169 1 : } else if (isThisWeek(timestamp, now)) {
170 : return FileRelativeTime.ThisWeek;
171 1 : } else if (isLastWeek(timestamp, now)) {
172 : return FileRelativeTime.LastWeek;
173 1 : } else if (isThisMonth(timestamp, now)) {
174 : return FileRelativeTime.ThisMonth;
175 1 : } else if (isLastMonth(timestamp, now)) {
176 : return FileRelativeTime.LastMonth;
177 1 : } else if (isThisYear(timestamp, now)) {
178 : return FileRelativeTime.ThisYear;
179 1 : } else if (isEarlierThanThisYear(timestamp, now)) {
180 : return FileRelativeTime.Earlier;
181 : } else {
182 0 : throw Exception("assignRelativeTime: impossible time comparison encountered, likely bug in cwtch-ui");
183 : }
184 : }
185 :
186 1 : String getLocalizedRelativeTime(dynamic localizer, FileRelativeTime t) {
187 : switch (t) {
188 1 : case FileRelativeTime.Today:
189 1 : return localizer!.relativeTimeToday;
190 1 : case FileRelativeTime.Yesterday:
191 1 : return localizer!.relativeTimeYesterday;
192 1 : case FileRelativeTime.ThisWeek:
193 0 : return localizer!.relativeTimeThisWeek;
194 1 : case FileRelativeTime.LastWeek:
195 1 : return localizer!.relativeTimeLastWeek;
196 1 : case FileRelativeTime.ThisMonth:
197 0 : return localizer!.relativeTimeThisMonth;
198 1 : case FileRelativeTime.LastMonth:
199 1 : return localizer!.relativeTimeLastMonth;
200 1 : case FileRelativeTime.ThisYear:
201 1 : return localizer!.relativeTimeThisYear;
202 1 : case FileRelativeTime.Earlier:
203 1 : return localizer!.relativeTimeEarlier;
204 : default:
205 : return "erroneous unlocalized time descriptor";
206 : }
207 : }
208 :
209 1 : List<dynamic> injectTimeHeaders(dynamic localizer, List<dynamic> sharedFiles, DateTime currentTime) {
210 1 : final result = <dynamic>[];
211 : var previousFileTime = FileRelativeTime.Today;
212 3 : for (var i = 0; i < sharedFiles.length; ++i) {
213 3 : final fileTime = DateTime.parse(sharedFiles[i]["DateShared"]);
214 1 : final relativeTime = assignRelativeTime(fileTime, currentTime);
215 1 : if (i == 0) {
216 : previousFileTime = relativeTime;
217 : }
218 1 : if (previousFileTime != relativeTime) {
219 4 : result.insert(result.length, {"TimeHeader": getLocalizedRelativeTime(localizer, previousFileTime)});
220 : previousFileTime = relativeTime;
221 : }
222 3 : result.insert(result.length, sharedFiles[i]);
223 3 : if (i == sharedFiles.length - 1) {
224 4 : result.insert(result.length, {"TimeHeader": getLocalizedRelativeTime(localizer, relativeTime)});
225 : }
226 : }
227 : return result;
228 : }
|