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