Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 8c637cf

Browse files
committed
Extract URL manipulation to its own file, and add unit tests.
1 parent 9af418f commit 8c637cf

File tree

3 files changed

+122
-31
lines changed

3 files changed

+122
-31
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/// A regular expression that matches against the "size directive" path
2+
/// segment of Google profile image URLs.
3+
///
4+
/// The format is is "`/sNN-c/`", where `NN` is the max width/height of the
5+
/// image, and "`c`" indicates we want the image cropped.
6+
final RegExp sizeDirective = RegExp(r'^s[0-9]{1,5}(-c)?$');
7+
8+
/// Adds sizing information to [photoUrl], inserted as the last path segment
9+
/// before the image filename. The format is described in [sizeDirective].
10+
///
11+
/// Falls back to the default profile photo if [photoUrl] is [null].
12+
String sizedProfileImageUrl(String photoUrl, double size) {
13+
if (photoUrl == null) {
14+
// If the user has no profile photo and no display name, fall back to
15+
// the default profile photo as a last resort.
16+
return 'https://lh3.googleusercontent.com/a/default-user=s${size.round()}-c';
17+
}
18+
final Uri profileUri = Uri.parse(photoUrl);
19+
final List<String> pathSegments = List<String>.from(profileUri.pathSegments);
20+
if (pathSegments.length <= 2) {
21+
/// New URLs may have directives at the end of the URL, like "`=sNN-c`".
22+
/// Each filter is separated by dashes. Filters may contain = signs:
23+
/// "`=s120-c-fSoften=1,50,0`"
24+
final String imagePath = pathSegments.last;
25+
// Locate the first =
26+
final int directiveSeparator = imagePath.indexOf('=');
27+
if (directiveSeparator >= 0) {
28+
// Split the image URL by the first =
29+
final String image = imagePath.substring(0, directiveSeparator);
30+
final String directive = imagePath.substring(directiveSeparator + 1);
31+
// Split the second half by -
32+
final Set<String> directives = Set<String>.from(directive.split('-'))
33+
// Remove the size directive, if present, and empty values
34+
..removeWhere((String s) => s.isEmpty || sizeDirective.hasMatch(s))
35+
// Add the size and crop directives
36+
..addAll(<String>['c', 's${size.round()}']);
37+
38+
pathSegments.last = '$image=${directives.join("-")}';
39+
} else {
40+
pathSegments.last = '${pathSegments.last}=c-s${size.round()}';
41+
}
42+
} else {
43+
// Old style URLs
44+
pathSegments
45+
..removeWhere(sizeDirective.hasMatch)
46+
..insert(pathSegments.length - 1, 's${size.round()}-c');
47+
}
48+
return Uri(
49+
scheme: profileUri.scheme,
50+
host: profileUri.host,
51+
pathSegments: pathSegments,
52+
).toString();
53+
}

packages/google_sign_in/lib/widgets.dart

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
88
import 'package:flutter/material.dart';
99

1010
import 'src/common.dart';
11+
import 'src/fife.dart' as fife;
1112

1213
/// Builds a CircleAvatar profile image of the appropriate resolution
1314
class GoogleUserCircleAvatar extends StatelessWidget {
@@ -33,7 +34,7 @@ class GoogleUserCircleAvatar extends StatelessWidget {
3334
///
3435
/// The format is is "`/sNN-c/`", where `NN` is the max width/height of the
3536
/// image, and "`c`" indicates we want the image cropped.
36-
static final RegExp sizeDirective = RegExp(r'^s[0-9]{1,5}(-c)?$');
37+
static final RegExp sizeDirective = fife.sizeDirective;
3738

3839
/// The Google user's identity; guaranteed to be non-null.
3940
final GoogleIdentity identity;
@@ -67,35 +68,6 @@ class GoogleUserCircleAvatar extends StatelessWidget {
6768
);
6869
}
6970

70-
/// Adds sizing information to [photoUrl], inserted as the last path segment
71-
/// before the image filename. The format is described in [sizeDirective].
72-
///
73-
/// Falls back to the default profile photo if [photoUrl] is [null].
74-
static String _sizedProfileImageUrl(String photoUrl, double size) {
75-
if (photoUrl == null) {
76-
// If the user has no profile photo and no display name, fall back to
77-
// the default profile photo as a last resort.
78-
return 'https://lh3.googleusercontent.com/a/default-user=s${size.round()}-c';
79-
}
80-
final Uri profileUri = Uri.parse(photoUrl);
81-
final List<String> pathSegments =
82-
List<String>.from(profileUri.pathSegments);
83-
if (pathSegments.length <= 2) {
84-
// New style URLs
85-
pathSegments.last = '${pathSegments.last}=s${size.round()}-c';
86-
} else {
87-
// Old style URLs
88-
pathSegments
89-
..removeWhere(sizeDirective.hasMatch)
90-
..insert(pathSegments.length - 1, 's${size.round()}-c');
91-
}
92-
return Uri(
93-
scheme: profileUri.scheme,
94-
host: profileUri.host,
95-
pathSegments: pathSegments,
96-
).toString();
97-
}
98-
9971
Widget _buildClippedImage(BuildContext context, BoxConstraints constraints) {
10072
assert(constraints.maxWidth == constraints.maxHeight);
10173

@@ -123,7 +95,7 @@ class GoogleUserCircleAvatar extends StatelessWidget {
12395
// Add a sizing directive to the profile photo URL.
12496
final double size =
12597
MediaQuery.of(context).devicePixelRatio * constraints.maxWidth;
126-
final String sizedPhotoUrl = _sizedProfileImageUrl(photoUrl, size);
98+
final String sizedPhotoUrl = fife.sizedProfileImageUrl(photoUrl, size);
12799

128100
// Fade the photo in over the top of the placeholder.
129101
return SizedBox(
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2019 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:google_sign_in/src/fife.dart';
7+
8+
void main() {
9+
group('sizedProfileImageUrl', () {
10+
const double size = 20;
11+
12+
group('Old style URLs', () {
13+
const String base =
14+
'https://lh3.googleusercontent.com/-ukEAtRyRhw8/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3rfhID9XACtdb9q_xK43VSXQvBV11Q.CMID';
15+
const String expected = '$base/s20-c/photo.jpg';
16+
17+
test('with directives, sets size', () {
18+
final String url = '$base/s64-c/photo.jpg';
19+
expect(sizedProfileImageUrl(url, size), expected);
20+
});
21+
22+
test('no directives, sets size and crop', () {
23+
final String url = '$base/photo.jpg';
24+
expect(sizedProfileImageUrl(url, size), expected);
25+
});
26+
27+
test('no crop, sets size and crop', () {
28+
final String url = '$base/s64/photo.jpg';
29+
expect(sizedProfileImageUrl(url, size), expected);
30+
});
31+
});
32+
33+
group('New style URLs', () {
34+
const String base =
35+
'https://lh3.googleusercontent.com/a-/AAuE7mC0Lh4F4uDtEaY7hpe-GIsbDpqfMZ3_2UhBQ8Qk';
36+
const String expected = '$base=c-s20';
37+
38+
test('with directives, sets size', () {
39+
final String url = '$base=s120-c';
40+
expect(sizedProfileImageUrl(url, size), expected);
41+
});
42+
43+
test('no directives, sets size and crop', () {
44+
final String url = base;
45+
expect(sizedProfileImageUrl(url, size), expected);
46+
});
47+
48+
test('no directives, but with an equals sign, sets size and crop', () {
49+
final String url = '$base=';
50+
expect(sizedProfileImageUrl(url, size), expected);
51+
});
52+
53+
test('no crop, adds crop', () {
54+
final String url = '$base=s120';
55+
expect(sizedProfileImageUrl(url, size), expected);
56+
});
57+
58+
test('many directives, sets size and crop, preserves other directives',
59+
() {
60+
final String url = '$base=s120-c-fSoften=1,50,0';
61+
final String expected = '$base=c-fSoften=1,50,0-s20';
62+
expect(sizedProfileImageUrl(url, size), expected);
63+
});
64+
});
65+
});
66+
}

0 commit comments

Comments
 (0)