frosty/lib/screens/channel/channel.dart
Tommy Chow 9a6892746d
Update to Material 3/You and other changes/fixes (#280)
* Use adaptive refresh indicators

* Add new message count indicator in chat

* Sync pubspec.lock

* Initial migration to material 3

* Restore default bottom sheet style

* Remove FrostyListTile and reset styles

* Use "Clear" button for all search history

* Add `require_trailing_commas` lint rule

* Exclude generated files from linter

* Use ListView for safe area support in bottom sheet

* Add initial dark theme

* Make sizing and units consistent

* Update onbaording spacings & copy

* Migrate to default material 3 buttons

* Add headers to chat details bottom sheet

* Simplify token log in method

* Use bottom sheet for account options

* Remove `AnimateScale` widget

* Tweak search paddings

* Update message spacing divisions

* Upgrade packages

* Update message spacing divisions

* Regenerate splash screens

* Update Inter & add google fonts

* Add initial material 3 themes

* Use adaptive alert dialogs

* Revert to oultined inputs

* Fix tooltip message

* Revert add google_fonts

* Improve themes and various styling

* Convert categories grid to list

* Tweak stream card thumbnail radii

* Tweak settings headers

* Left align floating snackbars

* Tweak search header padding

* Fix profile pics occasionally not rounded

* Use profile pic for settings button if logged in

* Redesign sleep timer

* Use divider theme and reduce thickness

* Use 0 letter spacing for tab bar titles

* Redesign category streams list

* Add border to tooltips

* Increase category card font weight

* Use outlined icon btton for chat user modal

* Reduce profile pic radius in search results

* Fix scrolll to top button safe area

* Add support for reply threads

* Reduce tooltip and snackbar border thickness

* Rebuild reply threads on new messages

* Upgrade packages (iOS)

* Reduce font weight for all chat alerts

* Use tabbars for pageview

* Add dividers everywhere

* Revert "Add dividers everywhere"

This reverts commit 5f0d349fdb.

* Remove fill from inputs

* Add dividers to settings

* Use segmented button for settings list

* Add scrollbar to lists with scrollcontroller

* Redesign badge/emote tooltip

* Remove outline from chat user modal buttons

* Remove divider from reply thread

* Use tap for chatters list and increase font weight

* Constrain emote tooltip width
2023-10-22 18:38:32 -04:00

348 lines
12 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/apis/bttv_api.dart';
import 'package:frosty/apis/ffz_api.dart';
import 'package:frosty/apis/seventv_api.dart';
import 'package:frosty/apis/twitch_api.dart';
import 'package:frosty/constants.dart';
import 'package:frosty/screens/channel/chat/chat.dart';
import 'package:frosty/screens/channel/chat/details/chat_details_store.dart';
import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
import 'package:frosty/screens/channel/video/video.dart';
import 'package:frosty/screens/channel/video/video_bar.dart';
import 'package:frosty/screens/channel/video/video_overlay.dart';
import 'package:frosty/screens/channel/video/video_store.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/theme.dart';
import 'package:frosty/widgets/app_bar.dart';
import 'package:frosty/widgets/notification.dart';
import 'package:provider/provider.dart';
import 'package:simple_pip_mode/actions/pip_actions_layout.dart';
import 'package:simple_pip_mode/pip_widget.dart';
/// Creates a widget that shows the video stream (if live) and chat of the given user.
class VideoChat extends StatefulWidget {
final String userId;
final String userName;
final String userLogin;
const VideoChat({
Key? key,
required this.userId,
required this.userName,
required this.userLogin,
}) : super(key: key);
@override
State<VideoChat> createState() => _VideoChatState();
}
class _VideoChatState extends State<VideoChat> {
final _videoKey = GlobalKey();
final _chatKey = GlobalKey();
late final ChatStore _chatStore = ChatStore(
channelName: widget.userLogin,
channelId: widget.userId,
displayName: widget.userName,
auth: context.read<AuthStore>(),
settings: context.read<SettingsStore>(),
chatDetailsStore: ChatDetailsStore(
twitchApi: context.read<TwitchApi>(),
channelName: widget.userLogin,
),
assetsStore: ChatAssetsStore(
twitchApi: context.read<TwitchApi>(),
ffzApi: context.read<FFZApi>(),
bttvApi: context.read<BTTVApi>(),
sevenTVApi: context.read<SevenTVApi>(),
),
);
late final VideoStore _videoStore = VideoStore(
userLogin: widget.userLogin,
twitchApi: context.read<TwitchApi>(),
authStore: context.read<AuthStore>(),
settingsStore: context.read<SettingsStore>(),
);
@override
Widget build(BuildContext context) {
final orientation = MediaQuery.of(context).orientation;
final settingsStore = _chatStore.settings;
final appBar = FrostyAppBar(
title: Text(
regexEnglish.hasMatch(_chatStore.displayName)
? _chatStore.displayName
: '${_chatStore.displayName} (${_chatStore.channelName})',
),
);
final player = GestureDetector(
onLongPress: _videoStore.handleToggleOverlay,
child: Video(
key: _videoKey,
videoStore: _videoStore,
),
);
final overlay = GestureDetector(
onLongPress: _videoStore.handleToggleOverlay,
onDoubleTap: orientation == Orientation.landscape
? () => settingsStore.fullScreen = !settingsStore.fullScreen
: null,
onTap: () {
if (_chatStore.assetsStore.showEmoteMenu) {
_chatStore.assetsStore.showEmoteMenu = false;
} else {
if (_chatStore.textFieldFocusNode.hasFocus) {
_chatStore.textFieldFocusNode.unfocus();
} else {
_videoStore.handleVideoTap();
}
}
},
child: Observer(
builder: (_) {
final videoOverlay = VideoOverlay(
videoStore: _videoStore,
chatStore: _chatStore,
);
if (_videoStore.paused || _videoStore.streamInfo == null) {
return videoOverlay;
}
return AnimatedOpacity(
opacity: _videoStore.overlayVisible ? 1.0 : 0.0,
curve: Curves.ease,
duration: const Duration(milliseconds: 200),
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(settingsStore.overlayOpacity),
),
child: IgnorePointer(
ignoring: !_videoStore.overlayVisible,
child: videoOverlay,
),
),
);
},
),
);
final video = Observer(
builder: (context) {
if (!_videoStore.settingsStore.showOverlay) return player;
return Stack(
children: [
player,
overlay,
],
);
},
);
final chat = Observer(
builder: (context) {
final videoBarVisible = _videoStore.streamInfo != null &&
_chatStore.settings.showVideo &&
(_videoStore.paused || _videoStore.overlayVisible);
return Stack(
children: [
Chat(
key: _chatKey,
chatStore: _chatStore,
),
if (orientation == Orientation.portrait)
AnimatedOpacity(
opacity: videoBarVisible ? 1 : 0,
curve: Curves.ease,
duration: const Duration(milliseconds: 200),
child: IgnorePointer(
ignoring: !videoBarVisible,
child: ColoredBox(
color: Theme.of(context).scaffoldBackgroundColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_videoStore.streamInfo != null)
VideoBar(
streamInfo: _videoStore.streamInfo!,
tappableCategory: false,
),
const Divider(),
],
),
),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: _chatStore.notification != null
? Align(
alignment: _chatStore.settings.chatNotificationsOnBottom
? Alignment.bottomCenter
: Alignment.topCenter,
child: FrostyNotification(
message: _chatStore.notification!,
showPasteButton:
_chatStore.notification!.contains('copied'),
onButtonPressed: () async {
// Paste clipboard text into the text controller.
final data =
await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) {
_chatStore.textController.text = data.text!;
}
_chatStore.updateNotification('');
},
),
)
: null,
),
],
);
},
);
final videoChat = Scaffold(
body: Observer(
builder: (context) {
if (orientation == Orientation.landscape &&
!settingsStore.landscapeForceVerticalChat) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
final landscapeChat = AnimatedContainer(
curve: Curves.ease,
duration: const Duration(milliseconds: 200),
width: _chatStore.expandChat
? MediaQuery.of(context).size.width / 2
: MediaQuery.of(context).size.width *
_chatStore.settings.chatWidth,
color: _chatStore.settings.fullScreen
? Colors.black.withOpacity(
_chatStore.settings.fullScreenChatOverlayOpacity,
)
: Theme.of(context).scaffoldBackgroundColor,
child: chat,
);
final overlayChat = Visibility(
visible: settingsStore.fullScreenChatOverlay,
maintainState: true,
child: Theme(
data: FrostyThemes().dark,
child: DefaultTextStyle(
style: DefaultTextStyle.of(context)
.style
.copyWith(color: Colors.white),
child: landscapeChat,
),
),
);
return ColoredBox(
color: settingsStore.showVideo
? Colors.black
: Theme.of(context).scaffoldBackgroundColor,
child: SafeArea(
bottom: false,
left: (settingsStore.landscapeCutout ==
LandscapeCutoutType.both ||
settingsStore.landscapeCutout ==
LandscapeCutoutType.left)
? false
: true,
right: (settingsStore.landscapeCutout ==
LandscapeCutoutType.both ||
settingsStore.landscapeCutout ==
LandscapeCutoutType.right)
? false
: true,
child: settingsStore.showVideo
? settingsStore.fullScreen
? Stack(
children: [
player,
if (settingsStore.showOverlay)
Row(
children: settingsStore.landscapeChatLeftSide
? [overlayChat, Expanded(child: overlay)]
: [Expanded(child: overlay), overlayChat],
),
],
)
: Row(
children: settingsStore.landscapeChatLeftSide
? [landscapeChat, Expanded(child: video)]
: [Expanded(child: video), landscapeChat],
)
: Column(
children: [appBar, Expanded(child: chat)],
),
),
);
}
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
return SafeArea(
child: Column(
children: [
if (!settingsStore.showVideo)
appBar
else
AspectRatio(aspectRatio: 16 / 9, child: video),
Expanded(child: chat),
],
),
);
},
),
);
// If on Android, use PiPSwitcher to enable PiP functionality.
if (Platform.isAndroid) {
return PipWidget(
pipLayout: PipActionsLayout.media_only_pause,
onPipAction: (_) => _videoStore.handlePausePlay(),
pipChild: player,
child: videoChat,
);
}
return videoChat;
}
@override
void dispose() {
_chatStore.dispose();
_videoStore.dispose();
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
SystemChrome.setPreferredOrientations([]);
super.dispose();
}
}