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