Merge pull request #629 from ente-io/update_dialog
Support for showing ChangeLog
This commit is contained in:
commit
4803c72776
8 changed files with 375 additions and 19 deletions
|
@ -15,6 +15,8 @@ class UpdateService {
|
|||
|
||||
static final UpdateService instance = UpdateService._privateConstructor();
|
||||
static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
|
||||
static const changeLogVersionKey = "update_change_log_key";
|
||||
static const currentChangeLogVersion = 1;
|
||||
|
||||
LatestVersionInfo _latestVersion;
|
||||
final _logger = Logger("UpdateService");
|
||||
|
@ -26,6 +28,20 @@ class UpdateService {
|
|||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
Future<bool> showChangeLog() async {
|
||||
// fetch the change log version which was last shown to user.
|
||||
final lastShownAtVersion = _prefs.getInt(changeLogVersionKey) ?? 0;
|
||||
return lastShownAtVersion < currentChangeLogVersion;
|
||||
}
|
||||
|
||||
Future<bool> hideChangeLog() async {
|
||||
return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
|
||||
}
|
||||
|
||||
Future<bool> resetChangeLog() {
|
||||
return _prefs.remove(changeLogVersionKey);
|
||||
}
|
||||
|
||||
Future<bool> shouldUpdate() async {
|
||||
if (!isIndependent()) {
|
||||
return false;
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:dots_indicator/dots_indicator.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/ui/account/email_entry_page.dart';
|
||||
import 'package:photos/ui/account/login_page.dart';
|
||||
import 'package:photos/ui/account/password_entry_page.dart';
|
||||
|
@ -152,6 +153,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
|||
}
|
||||
|
||||
void _navigateToSignUpPage() {
|
||||
UpdateService.instance.hideChangeLog().ignore();
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
page = const EmailEntryPage();
|
||||
|
@ -178,6 +180,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
|||
}
|
||||
|
||||
void _navigateToSignInPage() {
|
||||
UpdateService.instance.hideChangeLog().ignore();
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
page = const LoginPage();
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:move_to_background/move_to_background.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
|
@ -26,6 +27,7 @@ import 'package:photos/services/local_sync_service.dart';
|
|||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/states/user_details_state.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/collections_gallery_widget.dart';
|
||||
import 'package:photos/ui/common/bottom_shadow.dart';
|
||||
|
@ -39,6 +41,7 @@ import 'package:photos/ui/home/landing_page_widget.dart';
|
|||
import 'package:photos/ui/home/preserve_footer_widget.dart';
|
||||
import 'package:photos/ui/home/start_backup_hook_widget.dart';
|
||||
import 'package:photos/ui/loading_photos_widget.dart';
|
||||
import 'package:photos/ui/notification/update/change_log_page.dart';
|
||||
import 'package:photos/ui/settings/app_update_dialog.dart';
|
||||
import 'package:photos/ui/settings_page.dart';
|
||||
import 'package:photos/ui/shared_collections_gallery.dart';
|
||||
|
@ -172,6 +175,15 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
});
|
||||
// For sharing images coming from outside the app while the app is in the memory
|
||||
_initMediaShareSubscription();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() => {
|
||||
if (mounted) {showChangeLog(context)}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -414,4 +426,33 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
final ott = Uri.parse(link).queryParameters["ott"];
|
||||
UserService.instance.verifyEmail(context, ott);
|
||||
}
|
||||
|
||||
showChangeLog(BuildContext context) async {
|
||||
final bool show = await UpdateService.instance.showChangeLog();
|
||||
if (!show || !Configuration.instance.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
await showBarModalBottomSheet(
|
||||
topControl: const SizedBox.shrink(),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(5),
|
||||
topRight: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
backgroundColor: colorScheme.backgroundElevated,
|
||||
barrierColor: backdropMutedDark,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: const ChangeLogPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
// Do not show change dialog again
|
||||
UpdateService.instance.hideChangeLog().ignore();
|
||||
}
|
||||
}
|
||||
|
|
52
lib/ui/notification/update/change_log_entry.dart
Normal file
52
lib/ui/notification/update/change_log_entry.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
|
||||
class ChangeLogEntry {
|
||||
final bool isFeature;
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
ChangeLogEntry(this.title, this.description, {this.isFeature = true});
|
||||
}
|
||||
|
||||
class ChangeLogEntryWidget extends StatelessWidget {
|
||||
final ChangeLogEntry entry;
|
||||
|
||||
const ChangeLogEntryWidget({
|
||||
super.key,
|
||||
required this.entry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enteTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
entry.title,
|
||||
textAlign: TextAlign.left,
|
||||
style: enteTheme.largeBold.copyWith(
|
||||
color: entry.isFeature
|
||||
? colorScheme.primary700
|
||||
: colorScheme.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 18,
|
||||
),
|
||||
Text(
|
||||
entry.description,
|
||||
textAlign: TextAlign.left,
|
||||
style: enteTheme.body.copyWith(
|
||||
color: colorScheme.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 18,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
199
lib/ui/notification/update/change_log_page.dart
Normal file
199
lib/ui/notification/update/change_log_page.dart
Normal file
|
@ -0,0 +1,199 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/common/gradient_button.dart';
|
||||
import 'package:photos/ui/common/web_page.dart';
|
||||
import 'package:photos/ui/components/divider_widget.dart';
|
||||
import 'package:photos/ui/components/title_bar_title_widget.dart';
|
||||
import 'package:photos/ui/notification/update/change_log_entry.dart';
|
||||
|
||||
class ChangeLogPage extends StatefulWidget {
|
||||
const ChangeLogPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ChangeLogPage> createState() => _ChangeLogPageState();
|
||||
}
|
||||
|
||||
class _ChangeLogPageState extends State<ChangeLogPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enteColorScheme = getEnteColorScheme(context);
|
||||
final enteTextTheme = getEnteTextTheme(context);
|
||||
return Scaffold(
|
||||
appBar: null,
|
||||
body: Container(
|
||||
color: enteColorScheme.backgroundElevated,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 36,
|
||||
),
|
||||
SafeArea(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TitleBarTitleWidget(
|
||||
title: "What's new",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: _getChangeLog()),
|
||||
const DividerWidget(
|
||||
dividerType: DividerType.solid,
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: GradientButton(
|
||||
onTap: () async {
|
||||
await UpdateService.instance.hideChangeLog();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
text: "Let's go",
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
top: 12,
|
||||
right: 12,
|
||||
bottom: 6,
|
||||
),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: "If you like ente, ",
|
||||
),
|
||||
TextSpan(
|
||||
text: "let others know",
|
||||
style: enteTextTheme.small.copyWith(
|
||||
color: enteColorScheme.primary700,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// Single tapped.
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const WebPage(
|
||||
"Spread the word",
|
||||
"https://ente.io/share/",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
style: enteTextTheme.small,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getChangeLog() {
|
||||
final scrollController = ScrollController();
|
||||
final List<ChangeLogEntry> items = [];
|
||||
items.add(
|
||||
ChangeLogEntry(
|
||||
"Hide your photos!",
|
||||
"On popular demand, "
|
||||
"ente now supports photos that are hidden behind a lock.\n\nThis "
|
||||
"is in "
|
||||
"addition to the existing functionality to archive your photos so "
|
||||
"that they do not show in your timeline (but are otherwise visible)"
|
||||
".",
|
||||
),
|
||||
);
|
||||
items.add(
|
||||
ChangeLogEntry(
|
||||
'''Add a description to your photos''',
|
||||
"You can now add a caption / description to your photos and videos"
|
||||
".These will show up on the photo view.\n\nTo add a description, tap on the info icon to view the photo details and enter your text.",
|
||||
),
|
||||
);
|
||||
|
||||
items.add(
|
||||
ChangeLogEntry(
|
||||
'''And search photos descriptions too''',
|
||||
"Yes, it doesn't end there! You can also search your photos using "
|
||||
"their descriptions.\n\nThis allows you to, for example, tag your"
|
||||
" photos and quickly search for them.",
|
||||
),
|
||||
);
|
||||
if (Platform.isIOS) {
|
||||
items.add(
|
||||
ChangeLogEntry(
|
||||
'''Save live photos''',
|
||||
"There are some small fixes, including an enhancement to download and save live photos.",
|
||||
isFeature: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
items.add(
|
||||
ChangeLogEntry(
|
||||
'''Better import of WhatsApp photos''',
|
||||
"There are some small fixes, including an enhancement to use the creation time for photos imported from WhatsApp.",
|
||||
isFeature: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
thickness: 2.0,
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: ChangeLogEntryWidget(entry: items[index]),
|
||||
);
|
||||
},
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import 'package:photos/core/configuration.dart';
|
|||
import 'package:photos/core/network.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AppUpdateDialog extends StatefulWidget {
|
||||
final LatestVersionInfo latestVersionInfo;
|
||||
|
@ -21,11 +23,30 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> changelog = [];
|
||||
final enteTextTheme = getEnteTextTheme(context);
|
||||
final enteColor = getEnteColorScheme(context);
|
||||
for (final log in widget.latestVersionInfo.changelog) {
|
||||
changelog.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
|
||||
child: Text("- " + log, style: Theme.of(context).textTheme.caption),
|
||||
padding: const EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"- ",
|
||||
style: enteTextTheme.small.copyWith(color: enteColor.textMuted),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
log,
|
||||
softWrap: true,
|
||||
style:
|
||||
enteTextTheme.small.copyWith(color: enteColor.textMuted),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -34,20 +55,10 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.latestVersionInfo.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
"A new version of ente is available.",
|
||||
style: enteTextTheme.body.copyWith(color: enteColor.textMuted),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
const Text(
|
||||
"Changelog",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: changelog,
|
||||
|
@ -55,12 +66,12 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
const Padding(padding: EdgeInsets.all(8)),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
height: 56,
|
||||
child: OutlinedButton(
|
||||
style: Theme.of(context).outlinedButtonTheme.style.copyWith(
|
||||
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
|
||||
(Set<MaterialState> states) {
|
||||
return Theme.of(context).textTheme.subtitle1;
|
||||
return enteTextTheme.bodyBold;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -79,6 +90,22 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
Center(
|
||||
child: InkWell(
|
||||
child: Text(
|
||||
"Install manually",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
.copyWith(decoration: TextDecoration.underline),
|
||||
),
|
||||
onTap: () => launchUrlString(
|
||||
widget.latestVersionInfo.url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
final shouldForceUpdate =
|
||||
|
@ -86,8 +113,24 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
return WillPopScope(
|
||||
onWillPop: () async => !shouldForceUpdate,
|
||||
child: AlertDialog(
|
||||
title: Text(
|
||||
shouldForceUpdate ? "Critical update available" : "Update available",
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_awesome_outlined,
|
||||
size: 48,
|
||||
color: enteColor.strokeMuted,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
shouldForceUpdate
|
||||
? "Critical update available"
|
||||
: "Update available",
|
||||
style: enteTextTheme.h3Bold,
|
||||
),
|
||||
],
|
||||
),
|
||||
content: content,
|
||||
),
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:photos/core/configuration.dart';
|
|||
import 'package:photos/services/ignored_files_service.dart';
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/components/captioned_text_widget.dart';
|
||||
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
|
||||
|
@ -37,6 +38,7 @@ class DebugSectionWidget extends StatelessWidget {
|
|||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
await UpdateService.instance.resetChangeLog();
|
||||
_showKeyAttributesDialog(context);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -12,7 +12,7 @@ description: ente photos application
|
|||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.6.55+385
|
||||
version: 0.6.56+386
|
||||
|
||||
environment:
|
||||
sdk: '>=2.17.0 <3.0.0'
|
||||
|
|
Loading…
Add table
Reference in a new issue