Skip to content

Commit

Permalink
Refactored quick actions and notifications to use a unified ExternalA…
Browse files Browse the repository at this point in the history
…ction type
  • Loading branch information
nrubin29 committed Sep 26, 2022
1 parent 6ed979a commit 383abb7
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 167 deletions.
2 changes: 1 addition & 1 deletion ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -922,4 +922,4 @@
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
}
5 changes: 3 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import 'package:finale/util/image_id_cache.dart';
import 'package:finale/util/notifications.dart' as notifications;
import 'package:finale/util/preference.dart';
import 'package:finale/util/preferences.dart';
import 'package:finale/util/quick_actions_manager.dart';
import 'package:finale/util/quick_actions_manager.dart'
as quick_actions_manager;
import 'package:finale/util/theme.dart';
import 'package:finale/widgets/entity/lastfm/profile_stack.dart';
import 'package:finale/widgets/main/login_view.dart';
Expand All @@ -17,7 +18,7 @@ Future<void> main() async {
await Preference.setup();

if (isMobile) {
await QuickActionsManager().setup();
await quick_actions_manager.setup();
await background_task_manager.setup();
await notifications.setup();
}
Expand Down
61 changes: 61 additions & 0 deletions lib/util/external_actions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:finale/services/generic.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';

import 'profile_tab.dart';
import 'time_safe_stream.dart';

/// An action taken outside of the app that causes the app to open.
///
/// Sources are quick actions, iOS widgets, and notifications.
class ExternalAction {
final ExternalActionType type;
final dynamic value;

ExternalAction.scrobbleOnce()
: type = ExternalActionType.scrobbleOnce,
value = null;

ExternalAction.scrobbleContinuously()
: type = ExternalActionType.scrobbleContinuously,
value = null;

ExternalAction.viewAlbum(BasicAlbum album)
: type = ExternalActionType.viewAlbum,
value = album;

ExternalAction.viewArtist(BasicArtist artist)
: type = ExternalActionType.viewArtist,
value = artist;

ExternalAction.viewTrack(Track track)
: type = ExternalActionType.viewTrack,
value = track;

ExternalAction.viewTab(ProfileTab tab)
: type = ExternalActionType.viewTab,
value = tab;

ExternalAction.openSpotifyChecker()
: type = ExternalActionType.openSpotifyChecker,
value = null;
}

enum ExternalActionType {
scrobbleOnce,
scrobbleContinuously,
viewAlbum,
viewArtist,
viewTrack,
viewTab,
openSpotifyChecker,
}

// This stream needs to be open for the entire lifetime of the app.
// ignore: close_sinks
@protected
final externalActions = ReplaySubject<Timestamped<ExternalAction>>();

/// A stream of [ExternalAction]s that should be handled.
Stream<ExternalAction> get externalActionsStream =>
externalActions.timeSafeStream();
20 changes: 8 additions & 12 deletions lib/util/notifications.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
import 'package:finale/util/external_actions.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:rxdart/rxdart.dart';
import 'package:universal_io/io.dart';

import 'time_safe_stream.dart';

enum NotificationType {
spotifyCheckerOutOfSync(
'Spotify Checker', 'Spotify is not sending scrobbles to Last.fm!');
'Spotify Checker',
'Spotify is not sending scrobbles to Last.fm!',
ExternalAction.openSpotifyChecker);

final String title;
final String body;
final ExternalAction Function() externalActionFactory;

const NotificationType(this.title, this.body);
const NotificationType(this.title, this.body, this.externalActionFactory);
}

// This stream needs to be open for the entire lifetime of the app.
// ignore: close_sinks
final _notifications = ReplaySubject<Timestamped<NotificationType>>();

/// A stream of notifications that should be handled.
Stream<NotificationType> get notificationsStream =>
_notifications.timeSafeStream();

Future<void> setup() async {
const initializationSettings = InitializationSettings(
android: AndroidInitializationSettings('@drawable/music_note'),
Expand Down Expand Up @@ -69,5 +64,6 @@ Future<void> showNotification(NotificationType type) async {

@pragma('vm:entry-point')
void didReceiveNotification(NotificationResponse details) {
_notifications.addTimestamped(NotificationType.values[details.id!]);
externalActions.addTimestamped(
NotificationType.values[details.id!].externalActionFactory());
}
196 changes: 73 additions & 123 deletions lib/util/quick_actions_manager.dart
Original file line number Diff line number Diff line change
@@ -1,136 +1,86 @@
/// Handles [QuickActions] and initial links by firing the corresponding
/// [ExternalAction]s.
import 'package:finale/services/generic.dart';
import 'package:finale/util/profile_tab.dart';
import 'package:finale/util/external_actions.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:rxdart/rxdart.dart';
import 'package:uni_links/uni_links.dart';

import 'profile_tab.dart';
import 'time_safe_stream.dart';

class QuickAction {
final QuickActionType type;
final dynamic value;

QuickAction.scrobbleOnce()
: type = QuickActionType.scrobbleOnce,
value = null;

QuickAction.scrobbleContinuously()
: type = QuickActionType.scrobbleContinuously,
value = null;

QuickAction.viewAlbum(BasicAlbum album)
: type = QuickActionType.viewAlbum,
value = album;

QuickAction.viewArtist(BasicArtist artist)
: type = QuickActionType.viewArtist,
value = artist;

QuickAction.viewTrack(Track track)
: type = QuickActionType.viewTrack,
value = track;

QuickAction.viewTab(ProfileTab tab)
: type = QuickActionType.viewTab,
value = tab;
}
Future<void> setup() async {
const quickActions = QuickActions();
await quickActions.initialize((type) {
_handleLink(Uri(host: type));
});
await quickActions.setShortcutItems(const [
ShortcutItem(
type: 'scrobbleonce',
localizedTitle: 'Recognize song',
icon: 'add',
),
ShortcutItem(
type: 'scrobblecontinuously',
localizedTitle: 'Recognize continuously',
icon: 'all_inclusive',
),
]);

try {
final initialUri = await getInitialUri();
_handleLink(initialUri);
} on FormatException {
// Do nothing.
}

enum QuickActionType {
scrobbleOnce,
scrobbleContinuously,
viewAlbum,
viewArtist,
viewTrack,
viewTab,
uriLinkStream.listen((uri) {
_handleLink(uri);
});
}

class QuickActionsManager {
static QuickActionsManager? _instance;

factory QuickActionsManager() => _instance ??= QuickActionsManager._();

QuickActionsManager._();

// This stream needs to be open for the entire lifetime of the app.
// ignore: close_sinks
final _quickActions = ReplaySubject<Timestamped<QuickAction>>();

/// A stream of [QuickAction]s that should be handled.
Stream<QuickAction> get quickActionStream => _quickActions.timeSafeStream();

Future<void> setup() async {
const quickActions = QuickActions();
await quickActions.initialize((type) {
_handleLink(Uri(host: type));
});
await quickActions.setShortcutItems(const [
ShortcutItem(
type: 'scrobbleonce',
localizedTitle: 'Recognize song',
icon: 'add',
),
ShortcutItem(
type: 'scrobblecontinuously',
localizedTitle: 'Recognize continuously',
icon: 'all_inclusive',
),
]);

try {
final initialUri = await getInitialUri();
_handleLink(initialUri);
} on FormatException {
// Do nothing.
void _handleLink(Uri? uri) {
if (uri == null) {
return;
} else if (uri.host == 'scrobbleonce') {
externalActions.addTimestamped(ExternalAction.scrobbleOnce());
} else if (uri.host == 'scrobblecontinuously') {
externalActions.addTimestamped(ExternalAction.scrobbleContinuously());
} else if (uri.host == 'album') {
final name = uri.queryParameters['name']!;
final artist = uri.queryParameters['artist']!;
externalActions.addTimestamped(ExternalAction.viewAlbum(
ConcreteBasicAlbum(name, ConcreteBasicArtist(artist))));
} else if (uri.host == 'artist') {
final name = uri.queryParameters['name']!;
externalActions
.addTimestamped(ExternalAction.viewArtist(ConcreteBasicArtist(name)));
} else if (uri.host == 'track') {
final name = uri.queryParameters['name']!;
final artist = uri.queryParameters['artist']!;
externalActions.addTimestamped(
ExternalAction.viewTrack(BasicConcreteTrack(name, artist, null)));
} else if (uri.host == 'profiletab') {
final tabString = uri.queryParameters['tab'];
ProfileTab tab;

switch (tabString) {
case 'scrobble':
tab = ProfileTab.recentScrobbles;
break;
case 'artist':
tab = ProfileTab.topArtists;
break;
case 'album':
tab = ProfileTab.topAlbums;
break;
case 'track':
tab = ProfileTab.topTracks;
break;
default:
throw ArgumentError.value(tabString, 'tab', 'Unknown tab');
}

uriLinkStream.listen((uri) {
_handleLink(uri);
});
}

void _handleLink(Uri? uri) {
if (uri == null) {
return;
} else if (uri.host == 'scrobbleonce') {
_quickActions.addTimestamped(QuickAction.scrobbleOnce());
} else if (uri.host == 'scrobblecontinuously') {
_quickActions.addTimestamped(QuickAction.scrobbleContinuously());
} else if (uri.host == 'album') {
final name = uri.queryParameters['name']!;
final artist = uri.queryParameters['artist']!;
_quickActions.addTimestamped(QuickAction.viewAlbum(
ConcreteBasicAlbum(name, ConcreteBasicArtist(artist))));
} else if (uri.host == 'artist') {
final name = uri.queryParameters['name']!;
_quickActions
.addTimestamped(QuickAction.viewArtist(ConcreteBasicArtist(name)));
} else if (uri.host == 'track') {
final name = uri.queryParameters['name']!;
final artist = uri.queryParameters['artist']!;
_quickActions.addTimestamped(
QuickAction.viewTrack(BasicConcreteTrack(name, artist, null)));
} else if (uri.host == 'profiletab') {
final tabString = uri.queryParameters['tab'];
ProfileTab tab;

switch (tabString) {
case 'scrobble':
tab = ProfileTab.recentScrobbles;
break;
case 'artist':
tab = ProfileTab.topArtists;
break;
case 'album':
tab = ProfileTab.topAlbums;
break;
case 'track':
tab = ProfileTab.topTracks;
break;
default:
throw ArgumentError.value(tabString, 'tab', 'Unknown tab');
}

_quickActions.addTimestamped(QuickAction.viewTab(tab));
}
externalActions.addTimestamped(ExternalAction.viewTab(tab));
}
}
16 changes: 8 additions & 8 deletions lib/widgets/main/main_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:async';

import 'package:finale/services/generic.dart';
import 'package:finale/util/constants.dart';
import 'package:finale/util/quick_actions_manager.dart';
import 'package:finale/util/external_actions.dart';
import 'package:finale/widgets/entity/lastfm/album_view.dart';
import 'package:finale/widgets/entity/lastfm/artist_view.dart';
import 'package:finale/widgets/entity/lastfm/track_view.dart';
Expand All @@ -28,32 +28,32 @@ class _MainViewState extends State<MainView> {
@override
void initState() {
super.initState();
_subscription = QuickActionsManager().quickActionStream.listen((action) {
if (action.type == QuickActionType.scrobbleOnce ||
action.type == QuickActionType.scrobbleContinuously) {
_subscription = externalActionsStream.listen((action) {
if (action.type == ExternalActionType.scrobbleOnce ||
action.type == ExternalActionType.scrobbleContinuously) {
setState(() {
Navigator.popUntil(context, (route) => route.isFirst);
_index = 2;
});
} else if (action.type == QuickActionType.viewAlbum) {
} else if (action.type == ExternalActionType.viewAlbum) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AlbumView(album: action.value as BasicAlbum)),
);
} else if (action.type == QuickActionType.viewArtist) {
} else if (action.type == ExternalActionType.viewArtist) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ArtistView(artist: action.value as BasicArtist)),
);
} else if (action.type == QuickActionType.viewTrack) {
} else if (action.type == ExternalActionType.viewTrack) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => TrackView(track: action.value as Track)),
);
} else if (action.type == QuickActionType.viewTab) {
} else if (action.type == ExternalActionType.viewTab) {
setState(() {
Navigator.popUntil(context, (route) => route.isFirst);
_index = 0;
Expand Down
Loading

0 comments on commit 383abb7

Please sign in to comment.