34
README.md
|
|
@ -114,14 +114,18 @@
|
|||
- Search for channels and categories
|
||||
- Watch live streams with chat
|
||||
- Support for BTTV, FFZ, and 7TV emotes/badges
|
||||
- View local chat user message history
|
||||
- See the list of users in chat and filter them
|
||||
- Emote menu
|
||||
- Local chat user message history
|
||||
- Chatters list with filter
|
||||
- Theater and full-screen mode
|
||||
- Picture-in-picture mode (iOS only)
|
||||
- Sleep timer
|
||||
- Block and unblock users
|
||||
- Light, dark, and black (OLED) themes
|
||||
- Picture-in-picture mode (iOS only)
|
||||
- Customizable settings
|
||||
|
||||
And more!
|
||||
|
||||
## Motivation
|
||||
|
||||
A major problem with the official Twitch app for many users is that emotes from services such as [BetterTTV (BTTV)](https://chrome.google.com/webstore/detail/betterttv/ajopnjidmegmdimjlfnijceegpefgped), [FrankerFaceZ (FFZ)](https://chrome.google.com/webstore/detail/frankerfacez/fadndhdgpmmaapbmfcknlfgcflmmmieb), and [7TV](https://chrome.google.com/webstore/detail/7tv/ammjkodgmmoknidbanneddgankgfejfh) are not officially supported. Twitch is unaffiliated with these services, hence why they haven't been integrated officially.
|
||||
|
|
@ -146,20 +150,20 @@ Frosty aims to bring these emotes and other general quality of life features to
|
|||
|
||||
## FAQ
|
||||
|
||||
### Why do certain animations and scrolling appear to be janky?
|
||||
|
||||
Due to Flutter, there may be some stutter and jank on the first installation and launch. After using and moving around the app for a bit the jank will be mitigated through shader warmup/caching and should be minimal on subsequent launches. Watching a stream with a relatively fast chat for a couple of minutes usually resolves it for me.
|
||||
|
||||
### Why am I getting ads even though I'm subscribed to the channel or have Turbo?
|
||||
|
||||
Even if you've logged in to the app, you'll still have to log in to the WebView so that you can be identified when the stream plays. You can do so by going to the settings and then under the "Account" section tap the "Log in to WebView" button.
|
||||
|
||||
### How can I change the quality of the stream?
|
||||
### Can I change the quality of the stream?
|
||||
|
||||
On Android, you can change the stream quality by turning off the custom stream overlay in the settings and tapping the gear icon on the bottom right.
|
||||
|
||||
On iOS, sadly quality options aren't available through the native player and rely on an "auto" setting. There is no official API for getting the live stream URLs so specific quality options are not possible at this time.
|
||||
|
||||
### Why do certain animations and scrolling appear to be janky?
|
||||
|
||||
Due to the Flutter framework, there may be some stutter and jank on the first installation and launch. After using and moving around the app for a bit the jank will be mitigated through shader warmup/caching and should be minimal on subsequent launches. Watching a stream with a relatively fast chat for a couple of minutes usually resolves it.
|
||||
|
||||
### Why am I getting ads even though I'm subscribed to the channel or have Turbo?
|
||||
|
||||
Even if you've logged in to the app, you'll still have to log in to the WebView so that you can be identified when the stream plays. You can do so by going to the settings and then under the "Account" section tap the "Log in to WebView" button.
|
||||
|
||||
### Why is there a delay between the stream and chat?
|
||||
|
||||
On Android, the delay should be minimal. Try refreshing the player if you have a delay.
|
||||
|
|
@ -178,11 +182,13 @@ This is caused by the Flutter framework and is being worked on (see [here](https
|
|||
|
||||
I'm limited to what is available in the [Twitch API](https://dev.twitch.tv/docs/api/reference), so certain features from the Twitch web or mobile app (e.g., voting on predictions and category viewer count) are sadly not available at the moment.
|
||||
|
||||
Other features related to ad-blocking or utilizing private APIs will likely not be implemented officially because they would violate Twitch's terms of service. My highest priority currently is staying on the app stores and focusing on making features for those builds.
|
||||
|
||||
I'll try to add as many features as possible but occasionally I may take a break or be limited in time due to studies and personal reasons.
|
||||
|
||||
### I'd like to report a bug or request a new feature. Where can I do that?
|
||||
### Where can I report a bug or request a new feature?
|
||||
|
||||
You can open a new issue [here](https://github.com/tommyxchow/frosty/issues) with the appropriate labels and I'll take a look at it.
|
||||
You can open a new issue [here](https://github.com/tommyxchow/frosty/issues) with the appropriate labels (e.g., "bug" or "feature request") and I'll take a look at it.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/icons/TwitchGlitchWhite.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
19
assets/icons/logo.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2_12" x1="50" y1="0" x2="50" y2="100" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9146FF"/>
|
||||
<stop offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill="url(#paint0_linear_2_12)"/>
|
||||
<mask id="mask0_2_12" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="15" y="32" width="70" height="33">
|
||||
<rect x="15" y="32" width="69.7793" height="33" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2_12)">
|
||||
<path d="M6.24302 73.762L44.1917 35.8133C46.1718 33.8332 47.1619 32.8431 48.3035 32.4722C49.3077 32.1459 50.3895 32.1459 51.3937 32.4722C52.5353 32.8431 53.5254 33.8332 55.5054 35.8133L56.3666 36.6744L12.761 80.28L6.24302 73.762Z" fill="white"/>
|
||||
<path d="M44.2383 35.8133C46.2184 33.8332 47.2085 32.8432 48.3501 32.4722C49.3543 32.1459 50.4361 32.1459 51.4403 32.4722C52.5819 32.8432 53.572 33.8332 55.552 35.8133L93.5008 73.762L86.9827 80.28L43.3772 36.6745L44.2383 35.8133Z" fill="white"/>
|
||||
<g style="mix-blend-mode:lighten">
|
||||
<rect x="38.6633" y="42.4162" width="61.6676" height="9.21788" transform="rotate(45 38.6633 42.4162)" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "BrandingImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "BrandingImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "BrandingImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Frosty</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>frosty</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Frosty</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>frosty</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -264,7 +264,7 @@ class TwitchApi {
|
|||
if (userData.isNotEmpty) {
|
||||
return UserTwitch.fromJson(userData.first);
|
||||
} else {
|
||||
return Future.error('User does not exist');
|
||||
return Future.error('User does not exist!');
|
||||
}
|
||||
} else {
|
||||
return Future.error('Failed to get user: ${decoded['message']}');
|
||||
|
|
@ -326,6 +326,22 @@ class TwitchApi {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a [CategoriesTwitch] object containing the category info corresponding to the provided [gameId].
|
||||
Future<CategoriesTwitch> getCategory({
|
||||
required Map<String, String> headers,
|
||||
required String gameId,
|
||||
}) async {
|
||||
final url = Uri.parse('https://api.twitch.tv/helix/games?id=$gameId');
|
||||
|
||||
final response = await _client.get(url, headers: headers);
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (response.statusCode == 200) {
|
||||
return CategoriesTwitch.fromJson(decoded);
|
||||
} else {
|
||||
return Future.error('Failed to get category: ${decoded['message']}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [CategoriesTwitch] containing up to 20 categories/games closest matching the [query] and a cursor for further requests.
|
||||
Future<CategoriesTwitch> searchCategories({
|
||||
required Map<String, String> headers,
|
||||
|
|
@ -406,7 +422,7 @@ class TwitchApi {
|
|||
return [];
|
||||
}
|
||||
} else {
|
||||
return Future.error('User does not exist');
|
||||
return Future.error('User does not exist!');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,5 +19,11 @@ const zeroWidthEmotes = [
|
|||
'cvHazmat',
|
||||
];
|
||||
|
||||
// Regex for matching strings that contain lower or upper case English characters.
|
||||
/// Regex for matching strings that contain lower or upper case English characters.
|
||||
final regexEnglish = RegExp(r'[a-zA-Z]');
|
||||
|
||||
/// The default badge width and height.
|
||||
const defaultBadgeSize = 18.0;
|
||||
|
||||
/// The default emote width and height when none are provided.
|
||||
const defaultEmoteSize = 28.0;
|
||||
|
|
|
|||
|
|
@ -89,16 +89,16 @@ class MyApp extends StatelessWidget {
|
|||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.black,
|
||||
),
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: Colors.deepPurpleAccent,
|
||||
accentColor: const Color(0xff9146ff),
|
||||
),
|
||||
toggleableActiveColor: Colors.deepPurpleAccent,
|
||||
toggleableActiveColor: const Color(0xff9146ff),
|
||||
tabBarTheme: const TabBarTheme(
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
|
|
@ -116,16 +116,16 @@ class MyApp extends StatelessWidget {
|
|||
titleTextStyle: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: Colors.deepPurpleAccent,
|
||||
accentColor: const Color(0xff9146ff),
|
||||
),
|
||||
dialogBackgroundColor: Colors.grey.shade900,
|
||||
toggleableActiveColor: Colors.deepPurpleAccent,
|
||||
toggleableActiveColor: const Color(0xff9146ff),
|
||||
);
|
||||
|
||||
final oledTheme = ThemeData(
|
||||
|
|
@ -138,16 +138,16 @@ class MyApp extends StatelessWidget {
|
|||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: Colors.deepPurpleAccent,
|
||||
accentColor: const Color(0xff9146ff),
|
||||
),
|
||||
dialogBackgroundColor: Colors.black,
|
||||
toggleableActiveColor: Colors.deepPurpleAccent,
|
||||
toggleableActiveColor: const Color(0xff9146ff),
|
||||
);
|
||||
|
||||
return Observer(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class CategoryTwitch {
|
|||
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
|
||||
class CategoriesTwitch {
|
||||
final List<CategoryTwitch> data;
|
||||
final Map<String, String> pagination;
|
||||
final Map<String, String>? pagination;
|
||||
|
||||
const CategoriesTwitch(
|
||||
this.data,
|
||||
|
|
|
|||
|
|
@ -18,5 +18,7 @@ CategoriesTwitch _$CategoriesTwitchFromJson(Map<String, dynamic> json) =>
|
|||
(json['data'] as List<dynamic>)
|
||||
.map((e) => CategoryTwitch.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
Map<String, String>.from(json['pagination'] as Map),
|
||||
(json['pagination'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -291,6 +291,8 @@ class Emote {
|
|||
factory Emote.fromFFZ(EmoteFFZ emote, EmoteType type) => Emote(
|
||||
name: emote.name,
|
||||
zeroWidth: false,
|
||||
width: emote.width,
|
||||
height: emote.height,
|
||||
url: 'https:' + (emote.urls.url4x ?? emote.urls.url2x ?? emote.urls.url1x),
|
||||
type: type,
|
||||
);
|
||||
|
|
@ -308,6 +310,22 @@ class Emote {
|
|||
Map<String, dynamic> toJson() => _$EmoteToJson(this);
|
||||
}
|
||||
|
||||
const emoteType = [
|
||||
'Twitch (Bits Tier)',
|
||||
'Twitch (Follower)',
|
||||
'Twitch (Subscriber)',
|
||||
'Twitch (Global)',
|
||||
'Twitch (Unlocked)',
|
||||
'Twitch (Channel)',
|
||||
'FFZ (Global)',
|
||||
'FFZ (Channel)',
|
||||
'BTTV (Global)',
|
||||
'BTTV (Channel)',
|
||||
'BTTV (Shared)',
|
||||
'7TV (Global)',
|
||||
'7TV (Channel)',
|
||||
];
|
||||
|
||||
enum EmoteType {
|
||||
twitchBits,
|
||||
twitchFollower,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/models/badges.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
|
|
@ -81,8 +82,8 @@ class IRCMessage {
|
|||
List<InlineSpan> generateSpan({
|
||||
required TextStyle? style,
|
||||
required ChatAssetsStore assetsStore,
|
||||
required double badgeHeight,
|
||||
required double emoteHeight,
|
||||
required double badgeScale,
|
||||
required double emoteScale,
|
||||
required bool isLightTheme,
|
||||
bool showMessage = true,
|
||||
bool useZeroWidth = false,
|
||||
|
|
@ -95,6 +96,8 @@ class IRCMessage {
|
|||
final sevenTVUserToBadges = assetsStore.userTo7TVBadges;
|
||||
final bttvUserToBadge = assetsStore.userToBTTVBadges;
|
||||
final ffzRoomInfo = assetsStore.ffzRoomInfo;
|
||||
final badgeSize = defaultBadgeSize * badgeScale;
|
||||
final emoteSize = defaultEmoteSize * emoteScale;
|
||||
|
||||
// The span list that will be used to render the chat message
|
||||
final span = <InlineSpan>[];
|
||||
|
|
@ -156,7 +159,7 @@ class IRCMessage {
|
|||
span.add(
|
||||
_createBadgeSpan(
|
||||
badge: newBadge,
|
||||
height: badgeHeight,
|
||||
size: badgeSize,
|
||||
backgroundColor: const Color(0xFF00AD03),
|
||||
),
|
||||
);
|
||||
|
|
@ -178,7 +181,7 @@ class IRCMessage {
|
|||
span.add(
|
||||
_createBadgeSpan(
|
||||
badge: newBadge,
|
||||
height: badgeHeight,
|
||||
size: badgeSize,
|
||||
),
|
||||
);
|
||||
span.add(const TextSpan(text: ' '));
|
||||
|
|
@ -197,7 +200,7 @@ class IRCMessage {
|
|||
0,
|
||||
_createBadgeSpan(
|
||||
badge: badge,
|
||||
height: badgeHeight,
|
||||
size: badgeSize,
|
||||
backgroundColor: color,
|
||||
),
|
||||
);
|
||||
|
|
@ -207,7 +210,7 @@ class IRCMessage {
|
|||
span.add(
|
||||
_createBadgeSpan(
|
||||
badge: badge,
|
||||
height: badgeHeight,
|
||||
size: badgeSize,
|
||||
backgroundColor: color,
|
||||
),
|
||||
);
|
||||
|
|
@ -222,7 +225,7 @@ class IRCMessage {
|
|||
span.add(
|
||||
_createBadgeSpan(
|
||||
badge: userBTTVBadge,
|
||||
height: badgeHeight,
|
||||
size: badgeSize,
|
||||
isSvg: true,
|
||||
),
|
||||
);
|
||||
|
|
@ -236,7 +239,7 @@ class IRCMessage {
|
|||
span.add(
|
||||
_createBadgeSpan(
|
||||
badge: badge,
|
||||
height: badgeHeight,
|
||||
size: badgeSize,
|
||||
),
|
||||
);
|
||||
span.add(const TextSpan(text: ' '));
|
||||
|
|
@ -319,21 +322,20 @@ class IRCMessage {
|
|||
if (nextWordIsEmoji)
|
||||
Text(
|
||||
words[index],
|
||||
style: textStyle?.copyWith(fontSize: emoteHeight - 5),
|
||||
style: textStyle?.copyWith(fontSize: emoteSize - 5),
|
||||
),
|
||||
...emoteStack.reversed.map(
|
||||
(emote) => CachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
fadeInDuration: const Duration(seconds: 0),
|
||||
height: emoteHeight,
|
||||
fadeInDuration: const Duration(),
|
||||
height: emote.height != null ? emote.height! * emoteScale : emoteSize,
|
||||
width: emote.width != null ? emote.width! * emoteScale : emoteSize,
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
// Create the message for the tooltip
|
||||
final message = emoteStack.reversed.map((emote) => emote.name).join(', ');
|
||||
final tooltip = nextWordIsEmoji ? words[index] + ', ' + message : message;
|
||||
final message = emoteStack.reversed.map((emote) => '${emote.name} - ${emoteType[emote.type.index]}');
|
||||
localSpan.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
|
|
@ -354,17 +356,26 @@ class IRCMessage {
|
|||
...emoteStack.reversed.map(
|
||||
(emote) => CachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
fadeInDuration: const Duration(seconds: 0),
|
||||
fadeInDuration: const Duration(),
|
||||
height: 80,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
tooltip,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
nextWordIsEmoji
|
||||
? '${words[index]} - Emoji'
|
||||
: '${emoteStack.last.name} - ${emoteType[emoteStack.last.type.index]}',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
Text(
|
||||
nextWordIsEmoji ? message.join(', ') : message.skip(1).join(', '),
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -388,13 +399,14 @@ class IRCMessage {
|
|||
localSpan.add(
|
||||
_createEmoteSpan(
|
||||
emote: emote,
|
||||
height: emoteHeight,
|
||||
height: emote.height != null ? emote.height! * emoteScale : emoteSize,
|
||||
width: emote.width != null ? emote.width! * emoteScale : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (regexEmoji.hasMatch(word)) {
|
||||
localSpan.add(_createEmojiSpan(emoji: word, style: textStyle?.copyWith(fontSize: emoteHeight - 5)));
|
||||
localSpan.add(_createEmojiSpan(emoji: word, style: textStyle?.copyWith(fontSize: emoteSize - 5)));
|
||||
} else {
|
||||
localSpan.add(_createTextSpan(text: word, style: textStyle));
|
||||
}
|
||||
|
|
@ -415,12 +427,13 @@ class IRCMessage {
|
|||
span.add(
|
||||
_createEmoteSpan(
|
||||
emote: emote,
|
||||
height: emoteHeight,
|
||||
height: emote.height != null ? emote.height! * emoteScale : emoteSize,
|
||||
width: emote.width != null ? emote.width! * emoteScale : null,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (regexEmoji.hasMatch(word)) {
|
||||
span.add(_createEmojiSpan(emoji: word, style: textStyle?.copyWith(fontSize: emoteHeight - 5)));
|
||||
span.add(_createEmojiSpan(emoji: word, style: textStyle?.copyWith(fontSize: emoteSize - 5)));
|
||||
} else {
|
||||
span.add(_createTextSpan(text: word, style: textStyle));
|
||||
}
|
||||
|
|
@ -448,7 +461,7 @@ class IRCMessage {
|
|||
|
||||
static Widget _createBadgeWidget({
|
||||
required Badge badge,
|
||||
required double height,
|
||||
required double size,
|
||||
Color? backgroundColor,
|
||||
bool? isSvg,
|
||||
}) {
|
||||
|
|
@ -457,30 +470,30 @@ class IRCMessage {
|
|||
color: backgroundColor,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: badge.url,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
fadeInDuration: const Duration(seconds: 0),
|
||||
height: height,
|
||||
fadeInDuration: const Duration(),
|
||||
height: size,
|
||||
width: size,
|
||||
),
|
||||
);
|
||||
} else if (isSvg == true) {
|
||||
return SvgPicture.network(
|
||||
badge.url,
|
||||
placeholderBuilder: (context) => const SizedBox(),
|
||||
height: height,
|
||||
height: size,
|
||||
width: size,
|
||||
);
|
||||
} else {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: badge.url,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
fadeInDuration: const Duration(seconds: 0),
|
||||
height: height,
|
||||
fadeInDuration: const Duration(),
|
||||
height: size,
|
||||
width: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static WidgetSpan _createBadgeSpan({
|
||||
required Badge badge,
|
||||
required double height,
|
||||
required double size,
|
||||
Color? backgroundColor,
|
||||
bool? isSvg,
|
||||
}) {
|
||||
|
|
@ -494,7 +507,7 @@ class IRCMessage {
|
|||
children: [
|
||||
_createBadgeWidget(
|
||||
badge: badge,
|
||||
height: 80,
|
||||
size: 80,
|
||||
backgroundColor: backgroundColor,
|
||||
isSvg: isSvg,
|
||||
),
|
||||
|
|
@ -510,7 +523,7 @@ class IRCMessage {
|
|||
preferBelow: false,
|
||||
child: _createBadgeWidget(
|
||||
badge: badge,
|
||||
height: height,
|
||||
size: size,
|
||||
backgroundColor: backgroundColor,
|
||||
isSvg: isSvg,
|
||||
),
|
||||
|
|
@ -521,23 +534,8 @@ class IRCMessage {
|
|||
static WidgetSpan _createEmoteSpan({
|
||||
required Emote emote,
|
||||
required double height,
|
||||
required double? width,
|
||||
}) {
|
||||
const emoteType = [
|
||||
'Twitch (Bits Tier)',
|
||||
'Twitch (Follower)',
|
||||
'Twitch (Subscriber)',
|
||||
'Twitch (Global)',
|
||||
'Twitch (Unlocked)',
|
||||
'Twitch (Channel)',
|
||||
'FFZ (Global)',
|
||||
'FFZ (Channel)',
|
||||
'BTTV (Global)',
|
||||
'BTTV (Channel)',
|
||||
'BTTV (Shared)',
|
||||
'7TV (Global)',
|
||||
'7TV (Channel)',
|
||||
];
|
||||
|
||||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Tooltip(
|
||||
|
|
@ -548,8 +546,7 @@ class IRCMessage {
|
|||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
fadeInDuration: const Duration(seconds: 0),
|
||||
fadeInDuration: const Duration(),
|
||||
height: 80,
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
|
|
@ -569,8 +566,9 @@ class IRCMessage {
|
|||
child: CachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
fadeInDuration: const Duration(seconds: 0),
|
||||
fadeInDuration: const Duration(),
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -33,47 +33,53 @@ class Chat extends StatelessWidget {
|
|||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize),
|
||||
child: Observer(
|
||||
builder: (context) => ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
addAutomaticKeepAlives: false,
|
||||
addRepaintBoundaries: false,
|
||||
itemCount: chatStore.messages.length,
|
||||
controller: chatStore.scrollController,
|
||||
separatorBuilder: (context, index) => SizedBox(height: chatStore.settings.messageSpacing),
|
||||
itemBuilder: (context, index) => Observer(
|
||||
builder: (context) {
|
||||
final message = chatStore.messages[index];
|
||||
builder: (context) {
|
||||
final showDividers = chatStore.settings.showChatMessageDividers;
|
||||
|
||||
if (message.user != null && message.user != chatStore.auth.user.details?.login) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false;
|
||||
},
|
||||
onLongPress: () => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
username: message.user!,
|
||||
chatStore: chatStore,
|
||||
userId: message.tags['user-id']!,
|
||||
displayName: message.tags['display-name']!,
|
||||
),
|
||||
),
|
||||
child: ChatMessage(
|
||||
ircMessage: message,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
settingsStore: chatStore.settings,
|
||||
),
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
addAutomaticKeepAlives: false,
|
||||
addRepaintBoundaries: false,
|
||||
itemCount: chatStore.messages.length,
|
||||
controller: chatStore.scrollController,
|
||||
separatorBuilder: (context, index) => showDividers
|
||||
? Divider(
|
||||
height: chatStore.settings.messageSpacing,
|
||||
thickness: 1.0,
|
||||
)
|
||||
: SizedBox(height: chatStore.settings.messageSpacing),
|
||||
itemBuilder: (context, index) => Observer(
|
||||
builder: (context) {
|
||||
final message = chatStore.messages[index];
|
||||
final chatMessage = ChatMessage(
|
||||
ircMessage: message,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
settingsStore: chatStore.settings,
|
||||
);
|
||||
}
|
||||
return ChatMessage(
|
||||
ircMessage: message,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
settingsStore: chatStore.settings,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
if (message.user != null && message.user != chatStore.auth.user.details?.login) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false;
|
||||
},
|
||||
onLongPress: () => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: chatStore,
|
||||
username: message.user!,
|
||||
userId: message.tags['user-id']!,
|
||||
displayName: message.tags['display-name']!,
|
||||
),
|
||||
),
|
||||
child: chatMessage,
|
||||
);
|
||||
}
|
||||
return chatMessage;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -101,10 +107,7 @@ class Chat extends StatelessWidget {
|
|||
if (chatStore.assetsStore.showEmoteMenu)
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height / 3,
|
||||
child: EmoteMenu(
|
||||
assetsStore: chatStore.assetsStore,
|
||||
textController: chatStore.textController,
|
||||
),
|
||||
child: EmoteMenu(chatStore: chatStore),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:frosty/screens/channel/chat/details/chat_modes.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_users_list.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_details_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
|
||||
class ChatDetails extends StatelessWidget {
|
||||
final ChatDetailsStore chatDetails;
|
||||
final ChatStore chatStore;
|
||||
final String userLogin;
|
||||
|
||||
const ChatDetails({
|
||||
Key? key,
|
||||
required this.chatDetails,
|
||||
required this.chatStore,
|
||||
required this.userLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
|
|
@ -29,6 +32,7 @@ class ChatDetails extends StatelessWidget {
|
|||
Expanded(
|
||||
child: ChattersList(
|
||||
chatDetails: chatDetails,
|
||||
chatStore: chatStore,
|
||||
userLogin: userLogin,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,18 +2,25 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/api/twitch_api.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_details_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ChattersList extends StatefulWidget {
|
||||
final ChatDetailsStore chatDetails;
|
||||
final ChatStore chatStore;
|
||||
final String userLogin;
|
||||
|
||||
const ChattersList({
|
||||
Key? key,
|
||||
required this.chatDetails,
|
||||
required this.chatStore,
|
||||
required this.userLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
|
|
@ -141,7 +148,24 @@ class _ChattersListState extends State<ChattersList> {
|
|||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => Text(users[index]),
|
||||
(context, index) => InkWell(
|
||||
child: Text(users[index]),
|
||||
onLongPress: () async {
|
||||
final userInfo = await context
|
||||
.read<TwitchApi>()
|
||||
.getUser(headers: context.read<AuthStore>().headersTwitch, userLogin: users[index]);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: widget.chatStore,
|
||||
username: userInfo.login,
|
||||
userId: userInfo.id,
|
||||
displayName: userInfo.displayName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
childCount: users.length,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_panel.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/recent_emotes_panel.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
|
||||
class EmoteMenu extends StatefulWidget {
|
||||
final ChatAssetsStore assetsStore;
|
||||
final TextEditingController textController;
|
||||
final ChatStore chatStore;
|
||||
|
||||
const EmoteMenu({
|
||||
Key? key,
|
||||
required this.assetsStore,
|
||||
required this.textController,
|
||||
required this.chatStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -20,7 +18,7 @@ class EmoteMenu extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _EmoteMenuState extends State<EmoteMenu> {
|
||||
late final PageController _pageContoller = PageController(initialPage: widget.assetsStore.emoteMenuIndex);
|
||||
late final PageController _pageContoller = PageController(initialPage: widget.chatStore.assetsStore.emoteMenuIndex);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -47,9 +45,9 @@ class _EmoteMenuState extends State<EmoteMenu> {
|
|||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
widget.assetsStore.emoteMenuIndex = index;
|
||||
widget.chatStore.assetsStore.emoteMenuIndex = index;
|
||||
},
|
||||
style: index == widget.assetsStore.emoteMenuIndex ? null : TextButton.styleFrom(primary: Colors.grey),
|
||||
style: index == widget.chatStore.assetsStore.emoteMenuIndex ? null : TextButton.styleFrom(primary: Colors.grey),
|
||||
child: Text(section),
|
||||
),
|
||||
),
|
||||
|
|
@ -59,39 +57,34 @@ class _EmoteMenuState extends State<EmoteMenu> {
|
|||
),
|
||||
Expanded(
|
||||
child: PageView(
|
||||
onPageChanged: (index) => widget.assetsStore.emoteMenuIndex = index,
|
||||
onPageChanged: (index) => widget.chatStore.assetsStore.emoteMenuIndex = index,
|
||||
controller: _pageContoller,
|
||||
children: [
|
||||
RecentEmotesPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
chatStore: widget.chatStore,
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
emotes: widget.assetsStore.userEmoteToObject.values.toList(),
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.userEmoteToObject.values.toList(),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
emotes: widget.assetsStore.bttvEmotes,
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.bttvEmotes,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
emotes: widget.assetsStore.ffzEmotes,
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.ffzEmotes,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
emotes: widget.assetsStore.sevenTVEmotes,
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.sevenTVEmotes,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_section.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class EmoteMenuPanel extends StatelessWidget {
|
||||
final ChatAssetsStore assetsStore;
|
||||
final TextEditingController textController;
|
||||
final ChatStore chatStore;
|
||||
final List<Emote> emotes;
|
||||
|
||||
const EmoteMenuPanel({
|
||||
Key? key,
|
||||
required this.assetsStore,
|
||||
required this.textController,
|
||||
required this.chatStore,
|
||||
required this.emotes,
|
||||
}) : super(key: key);
|
||||
|
||||
|
|
@ -48,8 +46,7 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
assetsStore: assetsStore,
|
||||
textController: textController,
|
||||
chatStore: chatStore,
|
||||
emotes: globalEmotes,
|
||||
),
|
||||
],
|
||||
|
|
@ -58,8 +55,7 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
child: SectionHeader('Channel Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
assetsStore: assetsStore,
|
||||
textController: textController,
|
||||
chatStore: chatStore,
|
||||
emotes: channelEmotes,
|
||||
),
|
||||
],
|
||||
|
|
@ -68,8 +64,7 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
child: SectionHeader('Subscribed Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
assetsStore: assetsStore,
|
||||
textController: textController,
|
||||
chatStore: chatStore,
|
||||
emotes: subEmotes,
|
||||
),
|
||||
],
|
||||
|
|
@ -78,8 +73,7 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
child: SectionHeader('Unlocked Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
assetsStore: assetsStore,
|
||||
textController: textController,
|
||||
chatStore: chatStore,
|
||||
emotes: miscEmotes,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,51 +1,50 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EmoteMenuSection extends StatelessWidget {
|
||||
final ChatAssetsStore assetsStore;
|
||||
final TextEditingController textController;
|
||||
final ChatStore chatStore;
|
||||
final List<Emote> emotes;
|
||||
|
||||
const EmoteMenuSection({
|
||||
Key? key,
|
||||
required this.assetsStore,
|
||||
required this.textController,
|
||||
required this.chatStore,
|
||||
required this.emotes,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
textController.text += ' ' + emotes[index].name;
|
||||
assetsStore.recentEmotes.remove(emotes[index]);
|
||||
assetsStore.recentEmotes.insert(0, emotes[index]);
|
||||
},
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
preferBelow: false,
|
||||
child: CachedNetworkImage(imageUrl: emotes[index].url),
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(emotes[index]),
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: emotes[index].url,
|
||||
height: emotes[index].height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emotes[index].width?.toDouble(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
childCount: emotes.length,
|
||||
),
|
||||
childCount: emotes.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,105 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecentEmotesPanel extends StatelessWidget {
|
||||
final ChatAssetsStore assetsStore;
|
||||
final TextEditingController textController;
|
||||
class RecentEmotesPanel extends StatefulWidget {
|
||||
final ChatStore chatStore;
|
||||
|
||||
const RecentEmotesPanel({
|
||||
Key? key,
|
||||
required this.assetsStore,
|
||||
required this.textController,
|
||||
required this.chatStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RecentEmotesPanel> createState() => _RecentEmotesPanelState();
|
||||
}
|
||||
|
||||
class _RecentEmotesPanelState extends State<RecentEmotesPanel> {
|
||||
Future<void> _showClearDialog() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Clear Recent Emotes'),
|
||||
content: const Text('Are you sure you want to clear your recent emotes?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Cancel'),
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(widget.chatStore.assetsStore.recentEmotes.clear);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'Recent Emotes',
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Recent Emotes',
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: TextButton(
|
||||
onPressed: widget.chatStore.assetsStore.recentEmotes.isEmpty ? null : _showClearDialog,
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final emote = assetsStore.recentEmotes[index];
|
||||
final validEmotes = [...assetsStore.emoteToObject.values, ...assetsStore.userEmoteToObject.values];
|
||||
final matchingEmotes = validEmotes.where((existingEmote) => existingEmote.name == emote.name);
|
||||
SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final emote = widget.chatStore.assetsStore.recentEmotes[index];
|
||||
final validEmotes = [...widget.chatStore.assetsStore.emoteToObject.values, ...widget.chatStore.assetsStore.userEmoteToObject.values];
|
||||
final matchingEmotes = validEmotes.where((existingEmote) => existingEmote.name == emote.name && existingEmote.type == emote.type);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: matchingEmotes.isNotEmpty
|
||||
? () {
|
||||
textController.text += ' ' + emote.name;
|
||||
assetsStore.recentEmotes.removeWhere((recentEmote) => recentEmote.name == matchingEmotes.first.name);
|
||||
assetsStore.recentEmotes.insert(0, matchingEmotes.first);
|
||||
}
|
||||
: null,
|
||||
child: Tooltip(
|
||||
message: emote.name,
|
||||
preferBelow: false,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: matchingEmotes.isNotEmpty ? matchingEmotes.first.url : emote.url,
|
||||
color: matchingEmotes.isNotEmpty ? null : const Color.fromRGBO(255, 255, 255, 0.5),
|
||||
colorBlendMode: matchingEmotes.isNotEmpty ? null : BlendMode.modulate,
|
||||
return InkWell(
|
||||
onTap: matchingEmotes.isNotEmpty ? () => widget.chatStore.addEmote(emote) : null,
|
||||
child: Tooltip(
|
||||
message: emote.name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: matchingEmotes.isNotEmpty ? matchingEmotes.first.url : emote.url,
|
||||
color: matchingEmotes.isNotEmpty ? null : const Color.fromRGBO(255, 255, 255, 0.5),
|
||||
colorBlendMode: matchingEmotes.isNotEmpty ? null : BlendMode.modulate,
|
||||
height: emote.height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emote.width?.toDouble(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: assetsStore.recentEmotes.length,
|
||||
addAutomaticKeepAlives: false,
|
||||
addRepaintBoundaries: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: widget.chatStore.assetsStore.recentEmotes.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_details.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
|
||||
|
|
@ -11,60 +13,105 @@ class ChatBottomBar extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) => Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.adaptive.more),
|
||||
tooltip: 'Chat Details',
|
||||
onPressed: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (_) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: ChatDetails(
|
||||
chatDetails: chatStore.chatDetailsStore,
|
||||
userLogin: chatStore.channelName,
|
||||
),
|
||||
builder: (context) {
|
||||
final emotes = [
|
||||
...chatStore.assetsStore.userEmoteToObject.values,
|
||||
...chatStore.assetsStore.bttvEmotes,
|
||||
...chatStore.assetsStore.ffzEmotes,
|
||||
...chatStore.assetsStore.sevenTVEmotes
|
||||
].where((emote) => emote.name.toLowerCase().contains(chatStore.textController.text.split(' ').last.toLowerCase())).toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (chatStore.settings.emoteAutocomplete && chatStore.showAutocomplete && emotes.isNotEmpty) ...[
|
||||
const Divider(
|
||||
height: 1.0,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
child: TextField(
|
||||
focusNode: chatStore.textFieldFocusNode,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
enabled: chatStore.auth.isLoggedIn ? true : false,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
tooltip: 'Emote Menu',
|
||||
icon: const Icon(Icons.emoji_emotions_outlined),
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatStore.assetsStore.showEmoteMenu = !chatStore.assetsStore.showEmoteMenu;
|
||||
},
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
itemCount: emotes.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(emotes[index], autocompleteMode: true),
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: emotes[index].url,
|
||||
fadeInDuration: const Duration(),
|
||||
height: emotes[index].height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emotes[index].width?.toDouble(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
isDense: true,
|
||||
hintMaxLines: 1,
|
||||
contentPadding: const EdgeInsets.all(10.0),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||
),
|
||||
hintText: chatStore.auth.isLoggedIn ? 'Send a message' : 'Log in to chat',
|
||||
),
|
||||
controller: chatStore.textController,
|
||||
onSubmitted: chatStore.sendMessage,
|
||||
),
|
||||
)
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.adaptive.more),
|
||||
tooltip: 'Chat Details',
|
||||
onPressed: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (_) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: ChatDetails(
|
||||
chatDetails: chatStore.chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: chatStore.channelName,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
child: TextField(
|
||||
focusNode: chatStore.textFieldFocusNode,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
enabled: chatStore.auth.isLoggedIn ? true : false,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
tooltip: 'Emote Menu',
|
||||
icon: const Icon(Icons.emoji_emotions_outlined),
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatStore.assetsStore.showEmoteMenu = !chatStore.assetsStore.showEmoteMenu;
|
||||
},
|
||||
),
|
||||
hintMaxLines: 1,
|
||||
contentPadding: const EdgeInsets.all(10.0),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||
),
|
||||
hintText: chatStore.auth.isLoggedIn ? 'Send a message' : 'Log in to chat',
|
||||
),
|
||||
controller: chatStore.textController,
|
||||
onSubmitted: chatStore.sendMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Send',
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: chatStore.auth.isLoggedIn ? () => chatStore.sendMessage(chatStore.textController.text) : null,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Send',
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: chatStore.auth.isLoggedIn ? () => chatStore.sendMessage(chatStore.textController.text) : null,
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ class ChatMessage extends StatelessWidget {
|
|||
children: ircMessage.generateSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
assetsStore: assetsStore,
|
||||
emoteHeight: settingsStore.emoteHeight,
|
||||
badgeHeight: settingsStore.badgeHeight,
|
||||
emoteScale: settingsStore.emoteScale,
|
||||
badgeScale: settingsStore.badgeScale,
|
||||
useZeroWidth: settingsStore.showZeroWidth,
|
||||
useReadableColors: settingsStore.useReadableColors,
|
||||
isLightTheme: Theme.of(context).brightness == Brightness.light,
|
||||
|
|
@ -70,8 +70,8 @@ class ChatMessage extends StatelessWidget {
|
|||
children: ircMessage.generateSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
assetsStore: assetsStore,
|
||||
emoteHeight: settingsStore.emoteHeight,
|
||||
badgeHeight: settingsStore.badgeHeight,
|
||||
emoteScale: settingsStore.emoteScale,
|
||||
badgeScale: settingsStore.badgeScale,
|
||||
showMessage: settingsStore.showDeletedMessages,
|
||||
useZeroWidth: settingsStore.showZeroWidth,
|
||||
useReadableColors: settingsStore.useReadableColors,
|
||||
|
|
@ -126,8 +126,8 @@ class ChatMessage extends StatelessWidget {
|
|||
children: ircMessage.generateSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
assetsStore: assetsStore,
|
||||
emoteHeight: settingsStore.emoteHeight,
|
||||
badgeHeight: settingsStore.badgeHeight,
|
||||
emoteScale: settingsStore.emoteScale,
|
||||
badgeScale: settingsStore.badgeScale,
|
||||
useZeroWidth: settingsStore.showZeroWidth,
|
||||
useReadableColors: settingsStore.useReadableColors,
|
||||
isLightTheme: Theme.of(context).brightness == Brightness.light,
|
||||
|
|
|
|||
|
|
@ -76,28 +76,34 @@ class ChatUserModal extends StatelessWidget {
|
|||
Expanded(
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: chatStore.settings.messageScale),
|
||||
child: ListView.separated(
|
||||
reverse: true,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onLongPress: () async {
|
||||
await Clipboard.setData(ClipboardData(text: userMessages[index].message));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Message copied!'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ChatMessage(
|
||||
ircMessage: userMessages[index],
|
||||
assetsStore: chatStore.assetsStore,
|
||||
settingsStore: chatStore.settings,
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize),
|
||||
child: ListView.separated(
|
||||
reverse: true,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onLongPress: () async {
|
||||
await Clipboard.setData(ClipboardData(text: userMessages[index].message));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Message copied!'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ChatMessage(
|
||||
ircMessage: userMessages[index],
|
||||
assetsStore: chatStore.assetsStore,
|
||||
settingsStore: chatStore.settings,
|
||||
),
|
||||
),
|
||||
separatorBuilder: (context, index) => chatStore.settings.showChatMessageDividers
|
||||
? Divider(
|
||||
height: chatStore.settings.messageSpacing,
|
||||
thickness: 1.0,
|
||||
)
|
||||
: SizedBox(height: chatStore.settings.messageSpacing),
|
||||
itemCount: userMessages.length,
|
||||
),
|
||||
separatorBuilder: (context, index) => SizedBox(
|
||||
height: chatStore.settings.messageSpacing,
|
||||
),
|
||||
itemCount: userMessages.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ abstract class _ChatStoreBase with Store {
|
|||
@readonly
|
||||
var _autoScroll = true;
|
||||
|
||||
@readonly
|
||||
var _showAutocomplete = false;
|
||||
|
||||
/// The logged-in user's appearance in chat.
|
||||
@readonly
|
||||
var _userState = const USERSTATE();
|
||||
|
|
@ -129,7 +132,7 @@ abstract class _ChatStoreBase with Store {
|
|||
}
|
||||
});
|
||||
|
||||
// Add a listener to the textfield that will do the following when focused:
|
||||
// Add a listener to the textfield focus that will do the following when focused:
|
||||
// 1. Hide the emote menu if it is currently shown
|
||||
// 2. Scroll to the latest message (after a brief delay)
|
||||
textFieldFocusNode.addListener(() {
|
||||
|
|
@ -142,6 +145,11 @@ abstract class _ChatStoreBase with Store {
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Add a listener to the textfield that will show/hide the autocomplete bar if focused.
|
||||
// Will also rebuild the autocomplete bar when typing, refreshing the results as the user types.
|
||||
textController
|
||||
.addListener(() => _showAutocomplete = textFieldFocusNode.hasFocus && textController.text.split(' ').last.isNotEmpty ? true : false);
|
||||
}
|
||||
|
||||
/// Handle and process the provided string-representation of the IRC data.
|
||||
|
|
@ -344,6 +352,28 @@ abstract class _ChatStoreBase with Store {
|
|||
textController.clear();
|
||||
}
|
||||
|
||||
/// Adds the given [emote] to the chat textfield.
|
||||
@action
|
||||
void addEmote(Emote emote, {bool autocompleteMode = false}) {
|
||||
if (textController.text.isEmpty || textController.text.endsWith(' ')) {
|
||||
textController.text += emote.name + ' ';
|
||||
} else if (autocompleteMode && _showAutocomplete && textController.text.endsWith('')) {
|
||||
final split = textController.text.split(' ')
|
||||
..removeLast()
|
||||
..add(emote.name + ' ');
|
||||
|
||||
textController.text = split.join(' ');
|
||||
} else {
|
||||
textController.text += ' ' + emote.name + ' ';
|
||||
}
|
||||
|
||||
assetsStore.recentEmotes
|
||||
..removeWhere((recentEmote) => recentEmote.name == emote.name && recentEmote.type == emote.type)
|
||||
..insert(0, emote);
|
||||
|
||||
textController.selection = TextSelection.fromPosition(TextPosition(offset: textController.text.length));
|
||||
}
|
||||
|
||||
/// Closes and disposes all the channels and controllers used by the store.
|
||||
void dispose() {
|
||||
_channel?.sink.close(1001);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,24 @@ mixin _$ChatStore on _ChatStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$_showAutocompleteAtom =
|
||||
Atom(name: '_ChatStoreBase._showAutocomplete');
|
||||
|
||||
bool get showAutocomplete {
|
||||
_$_showAutocompleteAtom.reportRead();
|
||||
return super._showAutocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _showAutocomplete => showAutocomplete;
|
||||
|
||||
@override
|
||||
set _showAutocomplete(bool value) {
|
||||
_$_showAutocompleteAtom.reportWrite(value, super._showAutocomplete, () {
|
||||
super._showAutocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$_userStateAtom = Atom(name: '_ChatStoreBase._userState');
|
||||
|
||||
USERSTATE get userState {
|
||||
|
|
@ -107,6 +125,17 @@ mixin _$ChatStore on _ChatStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addEmote(Emote emote, {bool autocompleteMode = false}) {
|
||||
final _$actionInfo = _$_ChatStoreBaseActionController.startAction(
|
||||
name: '_ChatStoreBase.addEmote');
|
||||
try {
|
||||
return super.addEmote(emote, autocompleteMode: autocompleteMode);
|
||||
} finally {
|
||||
_$_ChatStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ class _VideoOverlayState extends State<VideoOverlay> {
|
|||
if (widget.videoStore.sleepTimer != null && widget.videoStore.sleepTimer!.isActive)
|
||||
Row(
|
||||
children: [
|
||||
Text('Timer: ${widget.videoStore.timeRemaining.toString().split('.')[0]}'),
|
||||
const Icon(Icons.timer),
|
||||
Text(' ${widget.videoStore.timeRemaining.toString().split('.')[0]}'),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: 'Cancel Timer',
|
||||
|
|
@ -70,9 +71,10 @@ class _VideoOverlayState extends State<VideoOverlay> {
|
|||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Dismiss'),
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => TextButton(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: widget.videoStore.sleepHours == 0 && widget.videoStore.sleepMinutes == 0
|
||||
? null
|
||||
: () => widget.videoStore.updateSleepTimer(
|
||||
|
|
@ -82,7 +84,6 @@ class _VideoOverlayState extends State<VideoOverlay> {
|
|||
),
|
||||
),
|
||||
child: const Text('Set Timer'),
|
||||
style: TextButton.styleFrom(primary: Colors.green),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -260,14 +261,16 @@ class _VideoOverlayState extends State<VideoOverlay> {
|
|||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
'${widget.videoStore.streamInfo?.gameName} for ${NumberFormat().format(widget.videoStore.streamInfo?.viewerCount)} viewers',
|
||||
'${widget.videoStore.streamInfo?.gameName} \u2022 ${NumberFormat().format(widget.videoStore.streamInfo?.viewerCount)} viewers',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class _VideoChatState extends State<VideoChat> {
|
|||
final appBar = AppBar(
|
||||
title: Text(
|
||||
regexEnglish.hasMatch(_chatStore.displayName) ? _chatStore.displayName : _chatStore.displayName + ' (${_chatStore.channelName})',
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class _SearchState extends State<Search> {
|
|||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: 'Search for a channel or category',
|
||||
hintText: 'Find a channel or category',
|
||||
contentPadding: const EdgeInsets.all(10.0),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||
|
|
@ -64,12 +64,10 @@ class _SearchState extends State<Search> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_searchStore.searchHistory.isNotEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(15.0),
|
||||
child: Text(
|
||||
'HISTORY',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SectionHeader(
|
||||
'History',
|
||||
padding: headerPadding,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
|
|
@ -105,6 +103,7 @@ class _SearchState extends State<Search> {
|
|||
child: SectionHeader(
|
||||
'Channels',
|
||||
padding: headerPadding,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SearchResultsChannels(
|
||||
|
|
@ -115,6 +114,7 @@ class _SearchState extends State<Search> {
|
|||
child: SectionHeader(
|
||||
'Categories',
|
||||
padding: headerPadding,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SearchResultsCategories(searchStore: _searchStore),
|
||||
|
|
|
|||
|
|
@ -82,7 +82,10 @@ class SearchResultsChannels extends StatelessWidget {
|
|||
padding: const EdgeInsets.all(10.0),
|
||||
child: const Text(
|
||||
'LIVE',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:frosty/api/twitch_api.dart';
|
|||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/models/category.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'categories_store.g.dart';
|
||||
|
||||
class CategoriesStore = _CategoriesStoreBase with _$CategoriesStore;
|
||||
|
|
@ -68,7 +69,7 @@ abstract class _CategoriesStoreBase with Store {
|
|||
} else {
|
||||
_categories.addAll(result.data);
|
||||
}
|
||||
_categoriesCursor = result.pagination['cursor'];
|
||||
_categoriesCursor = result.pagination?['cursor'];
|
||||
|
||||
_error = null;
|
||||
} on SocketException {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class CategoryStreams extends StatelessWidget {
|
|||
centerTitle: true,
|
||||
title: Text(
|
||||
listStore.categoryInfo!.name,
|
||||
style: Theme.of(context).textTheme.headline6?.copyWith(fontWeight: FontWeight.bold),
|
||||
style: Theme.of(context).textTheme.headline6?.copyWith(fontWeight: FontWeight.w800),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
|
@ -95,6 +95,8 @@ class CategoryStreams extends StatelessWidget {
|
|||
width: thumbnailWidth,
|
||||
height: thumbnailHeight,
|
||||
showUptime: context.read<SettingsStore>().showThumbnailUptime,
|
||||
showThumbnail: context.read<SettingsStore>().showThumbnails,
|
||||
showCategory: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,10 +13,19 @@ class TopSection extends StatelessWidget {
|
|||
child: Column(
|
||||
children: const [
|
||||
TabBar(
|
||||
indicatorColor: Colors.deepPurpleAccent,
|
||||
indicatorColor: Color(0xff9146ff),
|
||||
tabs: [
|
||||
Tab(text: 'Streams'),
|
||||
Tab(text: 'Categories'),
|
||||
Tab(
|
||||
child: Text(
|
||||
'Streams',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'Categories',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
)),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/api/twitch_api.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:frosty/screens/channel/video_chat.dart';
|
||||
import 'package:frosty/screens/home/stores/list_store.dart';
|
||||
import 'package:frosty/screens/home/top/category_streams.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// A tappable card widget that displays a stream's thumbnail and details.
|
||||
class StreamCard extends StatelessWidget {
|
||||
|
|
@ -16,6 +20,8 @@ class StreamCard extends StatelessWidget {
|
|||
final int width;
|
||||
final int height;
|
||||
final bool showUptime;
|
||||
final bool showThumbnail;
|
||||
final bool showCategory;
|
||||
|
||||
const StreamCard({
|
||||
Key? key,
|
||||
|
|
@ -24,6 +30,8 @@ class StreamCard extends StatelessWidget {
|
|||
required this.width,
|
||||
required this.height,
|
||||
required this.showUptime,
|
||||
required this.showThumbnail,
|
||||
this.showCategory = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -63,28 +71,29 @@ class StreamCard extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0),
|
||||
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: showThumbnail ? 15.0 : 5.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: showUptime
|
||||
? Stack(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
thumbnail,
|
||||
Container(
|
||||
color: const Color.fromRGBO(0, 0, 0, 0.5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||
child: Text(
|
||||
DateTime.now().difference(DateTime.parse(streamInfo.startedAt)).toString().split('.')[0],
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
: thumbnail,
|
||||
),
|
||||
if (showThumbnail)
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: showUptime
|
||||
? Stack(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
thumbnail,
|
||||
Container(
|
||||
color: const Color.fromRGBO(0, 0, 0, 0.5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||
child: Text(
|
||||
DateTime.now().difference(DateTime.parse(streamInfo.startedAt)).toString().split('.')[0],
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
: thumbnail,
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
|
|
@ -105,7 +114,10 @@ class StreamCard extends StatelessWidget {
|
|||
preferBelow: false,
|
||||
child: Text(
|
||||
streamerName,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -120,14 +132,53 @@ class StreamCard extends StatelessWidget {
|
|||
child: Text(
|
||||
streamInfo.title.trim(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(streamInfo.gameName),
|
||||
const SizedBox(height: 5.0),
|
||||
if (showCategory) ...[
|
||||
InkWell(
|
||||
child: Tooltip(
|
||||
message: streamInfo.gameName,
|
||||
preferBelow: false,
|
||||
child: Text(
|
||||
streamInfo.gameName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
color: DefaultTextStyle.of(context).style.color?.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final category = await context
|
||||
.read<TwitchApi>()
|
||||
.getCategory(headers: context.read<AuthStore>().headersTwitch, gameId: streamInfo.gameId);
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CategoryStreams(
|
||||
listStore: ListStore(
|
||||
twitchApi: context.read<TwitchApi>(),
|
||||
authStore: context.read<AuthStore>(),
|
||||
listType: ListType.category,
|
||||
categoryInfo: category.data.first,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
],
|
||||
Text(
|
||||
'${NumberFormat().format(streamInfo.viewerCount)} viewers',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: DefaultTextStyle.of(context).style.color?.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ class _StreamsListState extends State<StreamsList> with AutomaticKeepAliveClient
|
|||
streamInfo: _listStore.streams[index],
|
||||
width: thumbnailWidth,
|
||||
height: thumbnailHeight,
|
||||
showThumbnail: context.read<SettingsStore>().showThumbnails,
|
||||
showUptime: context.read<SettingsStore>().showThumbnailUptime,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/blocked_users.dart';
|
||||
import 'package:frosty/screens/settings/widgets/profile_card.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:frosty/widgets/block_button.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class AccountSettings extends StatelessWidget {
|
||||
|
|
@ -20,13 +21,16 @@ class AccountSettings extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Account',
|
||||
padding: EdgeInsets.all(10.0),
|
||||
builder: (context) => ExpansionTile(
|
||||
leading: const Icon(Icons.account_circle),
|
||||
title: const Text(
|
||||
'Account',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
ProfileCard(authStore: authStore),
|
||||
if (authStore.isLoggedIn) ...[
|
||||
ListTile(
|
||||
|
|
@ -52,7 +56,7 @@ class AccountSettings extends StatelessWidget {
|
|||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Log In to WebView'),
|
||||
title: const Text('Log in to WebView'),
|
||||
),
|
||||
body: const WebView(
|
||||
initialUrl: 'https://www.twitch.tv/login',
|
||||
|
|
@ -69,3 +73,189 @@ class AccountSettings extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileCard extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const ProfileCard({Key? key, required this.authStore}) : super(key: key);
|
||||
|
||||
Future<void> _showLoginDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
children: [
|
||||
ColoredBox(
|
||||
color: const Color.fromRGBO(145, 70, 255, 0.8),
|
||||
child: SimpleDialogOption(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/icons/TwitchGlitchWhite.png',
|
||||
height: 25,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'Connect with Twitch',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
authStore.login();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Or',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
SimpleDialogOption(
|
||||
child: TextField(
|
||||
textAlign: TextAlign.center,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Token',
|
||||
),
|
||||
onSubmitted: (token) {
|
||||
authStore.login(customToken: token);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showLogoutDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Log Out'),
|
||||
content: const Text('Are you sure you want to log out?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Cancel'),
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
authStore.logout();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
if (authStore.error != null) {
|
||||
return ListTile(
|
||||
title: const Text('Failed to Connect'),
|
||||
trailing: OutlinedButton(
|
||||
onPressed: authStore.init,
|
||||
child: const Text('Try Again'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (authStore.isLoggedIn && authStore.user.details != null) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundImage: CachedNetworkImageProvider(authStore.user.details!.profileImageUrl),
|
||||
),
|
||||
title: Text(
|
||||
authStore.user.details!.displayName,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
trailing: OutlinedButton.icon(
|
||||
onPressed: () => _showLogoutDialog(context),
|
||||
icon: const Icon(Icons.logout_outlined),
|
||||
label: const Text('Log Out'),
|
||||
style: OutlinedButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: const Icon(
|
||||
Icons.no_accounts,
|
||||
size: 40,
|
||||
),
|
||||
title: const Text('Anonymous User'),
|
||||
subtitle: const Text('Log in to chat, view followed streams, and more.'),
|
||||
trailing: ElevatedButton.icon(
|
||||
onPressed: () => _showLoginDialog(context),
|
||||
icon: const Icon(Icons.login),
|
||||
label: const Text('Log In'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BlockedUsers extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const BlockedUsers({
|
||||
Key? key,
|
||||
required this.authStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Blocked Users'),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
await authStore.user.refreshBlockedUsers(headers: authStore.headersTwitch);
|
||||
},
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
if (authStore.user.blockedUsers.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('You don\'t have any blocked users.'),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
children: authStore.user.blockedUsers.map(
|
||||
(user) {
|
||||
final displayName = regexEnglish.hasMatch(user.displayName) ? user.displayName : user.displayName + ' (${user.userLogin})';
|
||||
return ListTile(
|
||||
title: Text(displayName),
|
||||
trailing: BlockButton(
|
||||
authStore: authStore,
|
||||
targetUser: displayName,
|
||||
targetUserId: user.userId,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
|
|
@ -23,165 +24,216 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
final settingsStore = widget.settingsStore;
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Chat',
|
||||
padding: EdgeInsets.all(10.0),
|
||||
builder: (context) => ExpansionTile(
|
||||
leading: const Icon(Icons.chat),
|
||||
title: const Text(
|
||||
'Chat',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Bottom Bar'),
|
||||
subtitle: const Text('Shows the bottom text field bar.'),
|
||||
value: settingsStore.showBottomBar,
|
||||
onChanged: (newValue) => settingsStore.showBottomBar = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Emote Autocomplete'),
|
||||
subtitle: const Text('Shows a bar that suggests matching emotes when typing.'),
|
||||
value: settingsStore.emoteAutocomplete,
|
||||
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.emoteAutocomplete = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Zero-Width Emotes'),
|
||||
subtitle: const Text('Shows "stacked" emotes from BetterTTV and 7TV.'),
|
||||
value: settingsStore.showZeroWidth,
|
||||
onChanged: (newValue) => settingsStore.showZeroWidth = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Readable Name Colors'),
|
||||
subtitle: const Text('Adjusts the lightness value of overly bright/dark names.'),
|
||||
value: settingsStore.useReadableColors,
|
||||
onChanged: (newValue) => settingsStore.useReadableColors = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Show Deleted Messages'),
|
||||
subtitle: const Text('Restores the original message of deleted messages.'),
|
||||
value: settingsStore.showDeletedMessages,
|
||||
onChanged: (newValue) => settingsStore.showDeletedMessages = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Message Dividers'),
|
||||
subtitle: const Text('Shows a subtle divider between each message.'),
|
||||
value: settingsStore.showChatMessageDividers,
|
||||
onChanged: (newValue) => settingsStore.showChatMessageDividers = newValue,
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: const Text('Message Timestamps'),
|
||||
subtitle: const Text('Shows timestamps for when a message was sent.'),
|
||||
trailing: DropdownButton(
|
||||
value: settingsStore.timestampType,
|
||||
onChanged: (TimestampType? newTimestamp) => settingsStore.timestampType = newTimestamp!,
|
||||
items: TimestampType.values
|
||||
.map((TimestampType value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(timestamps[value.index]),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Show bottom bar'),
|
||||
value: settingsStore.showBottomBar,
|
||||
onChanged: (newValue) => settingsStore.showBottomBar = newValue,
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
const SectionHeader('Message Appearance'),
|
||||
ExpansionTile(
|
||||
title: const Text(
|
||||
'Preview',
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Show deleted messages'),
|
||||
value: settingsStore.showDeletedMessages,
|
||||
onChanged: (newValue) => settingsStore.showDeletedMessages = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Show zero-width emotes'),
|
||||
subtitle: const Text('Makes "stacked" emotes from BetterTTV and 7TV visible in chat messages.'),
|
||||
value: settingsStore.showZeroWidth,
|
||||
onChanged: (newValue) => settingsStore.showZeroWidth = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Use readable colors for chat names'),
|
||||
subtitle: const Text('Adjusts the lightness value of overly bright/dark names in chat.'),
|
||||
value: settingsStore.useReadableColors,
|
||||
onChanged: (newValue) => settingsStore.useReadableColors = newValue,
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: const Text('Message timestamps'),
|
||||
subtitle: const Text('Displays timestamps for when a chat message was sent.'),
|
||||
trailing: DropdownButton(
|
||||
value: settingsStore.timestampType,
|
||||
onChanged: (TimestampType? newTimestamp) => settingsStore.timestampType = newTimestamp!,
|
||||
items: TimestampType.values
|
||||
.map((TimestampType value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(timestamps[value.index]),
|
||||
))
|
||||
.toList(),
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
margin: const EdgeInsets.all(10.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.deepPurple),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: settingsStore.fontSize),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/3',
|
||||
height: defaultBadgeSize * settingsStore.badgeScale,
|
||||
width: defaultBadgeSize * settingsStore.badgeScale,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' Badge and emote preview. '),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://static-cdn.jtvnw.net/emoticons/v2/425618/default/dark/3.0',
|
||||
height: defaultEmoteSize * settingsStore.emoteScale,
|
||||
width: defaultEmoteSize * settingsStore.emoteScale,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textScaleFactor: settingsStore.messageScale,
|
||||
),
|
||||
SizedBox(height: settingsStore.messageSpacing),
|
||||
Text(
|
||||
'Hello! Here\'s a text preview.',
|
||||
textScaleFactor: settingsStore.messageScale,
|
||||
),
|
||||
SizedBox(height: settingsStore.messageSpacing),
|
||||
Text(
|
||||
'And another for spacing without an emote!',
|
||||
textScaleFactor: settingsStore.messageScale,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
ExpansionTile(
|
||||
title: const Text('Message appearance'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
margin: const EdgeInsets.all(10.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.deepPurple),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: settingsStore.fontSize),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/3',
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
height: settingsStore.badgeHeight,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' Badge and emote preview '),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://static-cdn.jtvnw.net/emoticons/v2/425618/default/dark/3.0',
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
height: settingsStore.emoteHeight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textScaleFactor: settingsStore.messageScale,
|
||||
),
|
||||
SizedBox(height: settingsStore.messageSpacing),
|
||||
Text(
|
||||
'Hello! Here\'s a text preview.',
|
||||
textScaleFactor: settingsStore.messageScale,
|
||||
),
|
||||
SizedBox(height: settingsStore.messageSpacing),
|
||||
Text(
|
||||
'And another for spacing without an emote!',
|
||||
textScaleFactor: settingsStore.messageScale,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: Text('Message scale: ${settingsStore.messageScale.toStringAsFixed(1)}x'),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.messageScale,
|
||||
min: 0.5,
|
||||
max: 2.0,
|
||||
divisions: 15,
|
||||
onChanged: (newValue) => settingsStore.messageScale = newValue,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Message spacing: ${settingsStore.messageSpacing.toStringAsFixed(0)}'),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.messageSpacing,
|
||||
min: 0.0,
|
||||
max: 30.0,
|
||||
divisions: 30,
|
||||
onChanged: (newValue) => settingsStore.messageSpacing = newValue,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: Text('Font size: ${settingsStore.fontSize.toInt()}'),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.fontSize,
|
||||
min: 5,
|
||||
max: 20,
|
||||
divisions: 15,
|
||||
onChanged: (newValue) => settingsStore.fontSize = newValue,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Badge height: ${settingsStore.badgeHeight.toStringAsFixed(0)}'),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.badgeHeight,
|
||||
min: 10.0,
|
||||
max: 50.0,
|
||||
divisions: 8,
|
||||
onChanged: (newValue) => settingsStore.badgeHeight = newValue,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Emote height: ${settingsStore.emoteHeight.toStringAsFixed(0)}'),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.emoteHeight,
|
||||
min: 10.0,
|
||||
max: 50.0,
|
||||
divisions: 8,
|
||||
onChanged: (newValue) => settingsStore.emoteHeight = newValue,
|
||||
),
|
||||
),
|
||||
const Text('Badge Scale'),
|
||||
const Spacer(),
|
||||
Text('${settingsStore.badgeScale.toStringAsFixed(2)}x'),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.badgeScale,
|
||||
min: 0.25,
|
||||
max: 3.0,
|
||||
divisions: 11,
|
||||
onChanged: (newValue) => settingsStore.badgeScale = newValue,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Emote Scale'),
|
||||
const Spacer(),
|
||||
Text('${settingsStore.emoteScale.toStringAsFixed(2)}x'),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.emoteScale,
|
||||
min: 0.25,
|
||||
max: 3.0,
|
||||
divisions: 11,
|
||||
onChanged: (newValue) => settingsStore.emoteScale = newValue,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Message Scale'),
|
||||
const Spacer(),
|
||||
Text('${settingsStore.messageScale.toStringAsFixed(2)}x'),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.messageScale,
|
||||
min: 0.5,
|
||||
max: 2.0,
|
||||
divisions: 6,
|
||||
onChanged: (newValue) => settingsStore.messageScale = newValue,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Message Spacing'),
|
||||
const Spacer(),
|
||||
Text(settingsStore.messageSpacing.toStringAsFixed(0).toString()),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.messageSpacing,
|
||||
min: 0.0,
|
||||
max: 30.0,
|
||||
divisions: 6,
|
||||
onChanged: (newValue) => settingsStore.messageSpacing = newValue,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Font Size'),
|
||||
const Spacer(),
|
||||
Text(settingsStore.fontSize.toInt().toString()),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.fontSize,
|
||||
min: 5,
|
||||
max: 20,
|
||||
divisions: 15,
|
||||
onChanged: (newValue) => settingsStore.fontSize = newValue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class GeneralSettings extends StatelessWidget {
|
||||
final SettingsStore settingsStore;
|
||||
|
|
@ -13,35 +12,43 @@ class GeneralSettings extends StatelessWidget {
|
|||
const themes = ['System', 'Light', 'Dark', 'Black'];
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'General',
|
||||
padding: EdgeInsets.all(10.0),
|
||||
builder: (context) => ExpansionTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text(
|
||||
'General',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('Theme'),
|
||||
trailing: DropdownButton(
|
||||
value: settingsStore.themeType,
|
||||
onChanged: (ThemeType? newTheme) => settingsStore.themeType = newTheme!,
|
||||
items: ThemeType.values
|
||||
.map((ThemeType value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(themes[value.index]),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Theme'),
|
||||
trailing: DropdownButton(
|
||||
value: settingsStore.themeType,
|
||||
onChanged: (ThemeType? newTheme) => settingsStore.themeType = newTheme!,
|
||||
items: ThemeType.values
|
||||
.map((ThemeType value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(themes[value.index]),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Show stream uptime on thumbnails'),
|
||||
value: settingsStore.showThumbnailUptime,
|
||||
onChanged: (newValue) => settingsStore.showThumbnailUptime = newValue,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Stream Card Thumbnails'),
|
||||
value: settingsStore.showThumbnails,
|
||||
onChanged: (newValue) => settingsStore.showThumbnails = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Stream Uptime on Thumbnails'),
|
||||
subtitle: const Text('Shows the uptime of the stream in the HH:MM:SS format.'),
|
||||
value: settingsStore.showThumbnailUptime,
|
||||
onChanged: settingsStore.showThumbnails ? (newValue) => settingsStore.showThumbnailUptime = newValue : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OtherSettings extends StatelessWidget {
|
||||
final SettingsStore settingsStore;
|
||||
|
|
@ -14,15 +15,48 @@ class OtherSettings extends StatelessWidget {
|
|||
required this.settingsStore,
|
||||
}) : super(key: key);
|
||||
|
||||
Future<void> _showConfirmDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Reset All Settings'),
|
||||
content: const Text('Are you sure you want to reset all settings?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Cancel'),
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
settingsStore.reset();
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Settings reset!'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Other',
|
||||
padding: EdgeInsets.all(10.0),
|
||||
return ExpansionTile(
|
||||
leading: const Icon(Icons.help),
|
||||
title: const Text(
|
||||
'Other',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info),
|
||||
title: const Text('About Frosty'),
|
||||
|
|
@ -31,15 +65,24 @@ class OtherSettings extends StatelessWidget {
|
|||
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationIcon: SvgPicture.asset(
|
||||
'assets/icons/logo.svg',
|
||||
height: 80,
|
||||
),
|
||||
applicationName: packageInfo.appName,
|
||||
applicationVersion: 'Version ${packageInfo.version} (${packageInfo.buildNumber})',
|
||||
applicationLegalese: '\u{a9} 2022 Tommy Chow',
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.launch),
|
||||
title: const Text('FAQ'),
|
||||
onTap: () => launch('https://github.com/tommyxchow/frosty#faq'),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => SwitchListTile.adaptive(
|
||||
title: const Text('Send anonymous crash logs'),
|
||||
title: const Text('Send Anonymous Crash Logs'),
|
||||
value: settingsStore.sendCrashLogs,
|
||||
onChanged: (newValue) {
|
||||
if (newValue == true) {
|
||||
|
|
@ -52,9 +95,9 @@ class OtherSettings extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.delete_sweep),
|
||||
label: const Text('Clear Image Cache'),
|
||||
onPressed: () async {
|
||||
|
|
@ -68,6 +111,15 @@ class OtherSettings extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.restore),
|
||||
label: const Text('Reset All Settings'),
|
||||
onPressed: () => _showConfirmDialog(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class VideoSettings extends StatelessWidget {
|
||||
final SettingsStore settingsStore;
|
||||
|
|
@ -13,42 +12,45 @@ class VideoSettings extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Video',
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Show video'),
|
||||
value: settingsStore.showVideo,
|
||||
onChanged: (newValue) => settingsStore.showVideo = newValue,
|
||||
),
|
||||
builder: (context) => ExpansionTile(
|
||||
leading: const Icon(Icons.ondemand_video),
|
||||
title: const Text(
|
||||
'Video',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Video'),
|
||||
value: settingsStore.showVideo,
|
||||
onChanged: (newValue) => settingsStore.showVideo = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Custom Overlay'),
|
||||
subtitle: const Text('Replaces Twitch\'s default overlay with a mobile-friendly version.'),
|
||||
value: settingsStore.showOverlay,
|
||||
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.showOverlay = newValue : null,
|
||||
),
|
||||
if (Platform.isIOS)
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Use custom video overlay'),
|
||||
subtitle: const Text('Replaces the default video overlay with a mobile-friendly version.'),
|
||||
value: settingsStore.showOverlay,
|
||||
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.showOverlay = newValue : null,
|
||||
title: const Text('Picture-in-Picture Button (experimental)'),
|
||||
subtitle: const Text('Adds a button to enter PiP mode on the bottom right of the overlay. MAY CAUSE freezes/crashes.'),
|
||||
value: settingsStore.pictureInPicture,
|
||||
onChanged: settingsStore.showVideo && settingsStore.showOverlay ? (newValue) => settingsStore.pictureInPicture = newValue : null,
|
||||
),
|
||||
if (Platform.isIOS)
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Picture-in-picture button (experimental)'),
|
||||
subtitle: const Text('Adds a button to enter PiP mode on the bottom right of the overlay. May cause freezes/crashes.'),
|
||||
value: settingsStore.pictureInPicture,
|
||||
onChanged: settingsStore.showVideo && settingsStore.showOverlay ? (newValue) => settingsStore.pictureInPicture = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Long-press player to toggle overlay'),
|
||||
value: settingsStore.toggleableOverlay,
|
||||
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.toggleableOverlay = newValue : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Long-Press Player to Toggle Overlay'),
|
||||
subtitle: const Text('Allows switching between Twitch\'s default overlay and the custom overlay.'),
|
||||
value: settingsStore.toggleableOverlay,
|
||||
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.toggleableOverlay = newValue : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,6 @@ class Settings extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const divider = Divider(
|
||||
thickness: 1.0,
|
||||
indent: 10.0,
|
||||
endIndent: 10.0,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
|
|
@ -46,17 +40,14 @@ class Settings extends StatelessWidget {
|
|||
bottom: false,
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 10.0),
|
||||
AccountSettings(
|
||||
settingsStore: settingsStore,
|
||||
authStore: context.read<AuthStore>(),
|
||||
),
|
||||
divider,
|
||||
GeneralSettings(settingsStore: settingsStore),
|
||||
divider,
|
||||
VideoSettings(settingsStore: settingsStore),
|
||||
divider,
|
||||
ChatSettings(settingsStore: settingsStore),
|
||||
divider,
|
||||
OtherSettings(settingsStore: settingsStore),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -12,86 +12,177 @@ class SettingsStore extends _SettingsStoreBase with _$SettingsStore {
|
|||
}
|
||||
|
||||
abstract class _SettingsStoreBase with Store {
|
||||
//General Settings
|
||||
@JsonKey(defaultValue: ThemeType.system, unknownEnumValue: ThemeType.system)
|
||||
@observable
|
||||
var themeType = ThemeType.system;
|
||||
// * General Settings
|
||||
static const defaultThemeType = ThemeType.system;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
@observable
|
||||
var showThumbnailUptime = false;
|
||||
static const defaultShowThumbnailUptime = false;
|
||||
|
||||
// Video Settings
|
||||
@JsonKey(defaultValue: true)
|
||||
@observable
|
||||
var showVideo = true;
|
||||
static const defaultShowThumbnails = true;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
@JsonKey(defaultValue: defaultThemeType, unknownEnumValue: ThemeType.system)
|
||||
@observable
|
||||
var showOverlay = true;
|
||||
var themeType = defaultThemeType;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
@JsonKey(defaultValue: defaultShowThumbnailUptime)
|
||||
@observable
|
||||
var toggleableOverlay = false;
|
||||
var showThumbnailUptime = defaultShowThumbnailUptime;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
@JsonKey(defaultValue: defaultShowThumbnails)
|
||||
@observable
|
||||
var pictureInPicture = false;
|
||||
var showThumbnails = defaultShowThumbnails;
|
||||
|
||||
// Chat Settings
|
||||
@JsonKey(defaultValue: true)
|
||||
@observable
|
||||
var showBottomBar = true;
|
||||
// * Video Settings
|
||||
static const defaultShowVideo = true;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
@observable
|
||||
var showDeletedMessages = false;
|
||||
static const defaultShowOverlay = true;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
@observable
|
||||
var showZeroWidth = false;
|
||||
static const defaultToggleableOverlay = false;
|
||||
|
||||
@JsonKey(defaultValue: TimestampType.disabled, unknownEnumValue: TimestampType.disabled)
|
||||
@observable
|
||||
var timestampType = TimestampType.disabled;
|
||||
static const defaultPictureInPicture = false;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
@JsonKey(defaultValue: defaultShowVideo)
|
||||
@observable
|
||||
var useReadableColors = true;
|
||||
var showVideo = defaultShowVideo;
|
||||
|
||||
@JsonKey(defaultValue: 1.0)
|
||||
@JsonKey(defaultValue: defaultShowOverlay)
|
||||
@observable
|
||||
var messageScale = 1.0;
|
||||
var showOverlay = defaultShowOverlay;
|
||||
|
||||
@JsonKey(defaultValue: 14.0)
|
||||
@JsonKey(defaultValue: defaultToggleableOverlay)
|
||||
@observable
|
||||
var fontSize = 14.0;
|
||||
var toggleableOverlay = defaultToggleableOverlay;
|
||||
|
||||
@JsonKey(defaultValue: 10.0)
|
||||
@JsonKey(defaultValue: defaultPictureInPicture)
|
||||
@observable
|
||||
var messageSpacing = 10.0;
|
||||
var pictureInPicture = defaultPictureInPicture;
|
||||
|
||||
@JsonKey(defaultValue: 20.0)
|
||||
@observable
|
||||
var badgeHeight = 20.0;
|
||||
// * Chat Settings
|
||||
static const defaultShowBottomBar = true;
|
||||
|
||||
@JsonKey(defaultValue: 30.0)
|
||||
@observable
|
||||
var emoteHeight = 30.0;
|
||||
static const defaultShowDeletedMessages = false;
|
||||
|
||||
// Other settings
|
||||
@JsonKey(defaultValue: true)
|
||||
@observable
|
||||
var sendCrashLogs = true;
|
||||
static const defaultShowZeroWidth = false;
|
||||
|
||||
// Global configs
|
||||
@JsonKey(defaultValue: false)
|
||||
@observable
|
||||
var fullScreen = false;
|
||||
static const defaultShowChatMessageDividers = false;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
static const defaultTimestampType = TimestampType.disabled;
|
||||
|
||||
static const defaultUseReadableColors = true;
|
||||
|
||||
static const defaultMessageScale = 1.0;
|
||||
|
||||
static const defaultFontSize = 12.0;
|
||||
|
||||
static const defaultMessageSpacing = 10.0;
|
||||
|
||||
static const defaultBadgeScale = 1.0;
|
||||
|
||||
static const defaultEmoteScale = 1.0;
|
||||
|
||||
static const defaultEmoteAutocomplete = true;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowBottomBar)
|
||||
@observable
|
||||
var expandInfo = true;
|
||||
var showBottomBar = defaultShowBottomBar;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowDeletedMessages)
|
||||
@observable
|
||||
var showDeletedMessages = defaultShowDeletedMessages;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowZeroWidth)
|
||||
@observable
|
||||
var showZeroWidth = defaultShowZeroWidth;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowChatMessageDividers)
|
||||
@observable
|
||||
var showChatMessageDividers = defaultShowChatMessageDividers;
|
||||
|
||||
@JsonKey(defaultValue: defaultTimestampType, unknownEnumValue: TimestampType.disabled)
|
||||
@observable
|
||||
var timestampType = defaultTimestampType;
|
||||
|
||||
@JsonKey(defaultValue: defaultUseReadableColors)
|
||||
@observable
|
||||
var useReadableColors = defaultUseReadableColors;
|
||||
|
||||
@JsonKey(defaultValue: defaultFontSize)
|
||||
@observable
|
||||
var fontSize = defaultFontSize;
|
||||
|
||||
@JsonKey(defaultValue: defaultMessageSpacing)
|
||||
@observable
|
||||
var messageSpacing = defaultMessageSpacing;
|
||||
|
||||
@JsonKey(defaultValue: defaultMessageScale)
|
||||
@observable
|
||||
var messageScale = defaultMessageScale;
|
||||
|
||||
@JsonKey(defaultValue: defaultBadgeScale)
|
||||
@observable
|
||||
var badgeScale = defaultBadgeScale;
|
||||
|
||||
@JsonKey(defaultValue: defaultEmoteScale)
|
||||
@observable
|
||||
var emoteScale = defaultEmoteScale;
|
||||
|
||||
@JsonKey(defaultValue: defaultEmoteAutocomplete)
|
||||
@observable
|
||||
var emoteAutocomplete = defaultEmoteAutocomplete;
|
||||
|
||||
// * Other settings
|
||||
static const defaultSendCrashLogs = true;
|
||||
|
||||
@JsonKey(defaultValue: defaultSendCrashLogs)
|
||||
@observable
|
||||
var sendCrashLogs = defaultSendCrashLogs;
|
||||
|
||||
// * Global configs
|
||||
static const defaultFullScreen = false;
|
||||
|
||||
static const defaultExpandInfo = true;
|
||||
|
||||
@JsonKey(defaultValue: defaultFullScreen)
|
||||
@observable
|
||||
var fullScreen = defaultFullScreen;
|
||||
|
||||
@JsonKey(defaultValue: defaultExpandInfo)
|
||||
@observable
|
||||
var expandInfo = defaultExpandInfo;
|
||||
|
||||
@action
|
||||
void reset() {
|
||||
// * General Settings
|
||||
themeType = defaultThemeType;
|
||||
showThumbnailUptime = defaultShowThumbnailUptime;
|
||||
showThumbnails = defaultShowThumbnails;
|
||||
|
||||
// * Video Settings
|
||||
showVideo = defaultShowVideo;
|
||||
showOverlay = defaultShowOverlay;
|
||||
toggleableOverlay = defaultToggleableOverlay;
|
||||
pictureInPicture = defaultPictureInPicture;
|
||||
|
||||
// * Chat Settings
|
||||
showBottomBar = defaultShowBottomBar;
|
||||
showDeletedMessages = defaultShowDeletedMessages;
|
||||
showZeroWidth = defaultShowZeroWidth;
|
||||
showChatMessageDividers = defaultShowChatMessageDividers;
|
||||
timestampType = defaultTimestampType;
|
||||
useReadableColors = defaultUseReadableColors;
|
||||
messageScale = defaultMessageScale;
|
||||
fontSize = defaultFontSize;
|
||||
messageSpacing = defaultMessageSpacing;
|
||||
badgeScale = defaultBadgeScale;
|
||||
emoteScale = defaultEmoteScale;
|
||||
emoteAutocomplete = defaultEmoteAutocomplete;
|
||||
|
||||
// * Other settings
|
||||
sendCrashLogs = defaultSendCrashLogs;
|
||||
|
||||
// * Global configs
|
||||
fullScreen = defaultFullScreen;
|
||||
expandInfo = defaultExpandInfo;
|
||||
}
|
||||
}
|
||||
|
||||
enum ThemeType {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
unknownValue: ThemeType.system) ??
|
||||
ThemeType.system
|
||||
..showThumbnailUptime = json['showThumbnailUptime'] as bool? ?? false
|
||||
..showThumbnails = json['showThumbnails'] as bool? ?? true
|
||||
..showVideo = json['showVideo'] as bool? ?? true
|
||||
..showOverlay = json['showOverlay'] as bool? ?? true
|
||||
..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false
|
||||
|
|
@ -19,16 +20,19 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
..showBottomBar = json['showBottomBar'] as bool? ?? true
|
||||
..showDeletedMessages = json['showDeletedMessages'] as bool? ?? false
|
||||
..showZeroWidth = json['showZeroWidth'] as bool? ?? false
|
||||
..showChatMessageDividers =
|
||||
json['showChatMessageDividers'] as bool? ?? false
|
||||
..timestampType = $enumDecodeNullable(
|
||||
_$TimestampTypeEnumMap, json['timestampType'],
|
||||
unknownValue: TimestampType.disabled) ??
|
||||
TimestampType.disabled
|
||||
..useReadableColors = json['useReadableColors'] as bool? ?? true
|
||||
..messageScale = (json['messageScale'] as num?)?.toDouble() ?? 1.0
|
||||
..fontSize = (json['fontSize'] as num?)?.toDouble() ?? 14.0
|
||||
..fontSize = (json['fontSize'] as num?)?.toDouble() ?? 12.0
|
||||
..messageSpacing = (json['messageSpacing'] as num?)?.toDouble() ?? 10.0
|
||||
..badgeHeight = (json['badgeHeight'] as num?)?.toDouble() ?? 20.0
|
||||
..emoteHeight = (json['emoteHeight'] as num?)?.toDouble() ?? 30.0
|
||||
..messageScale = (json['messageScale'] as num?)?.toDouble() ?? 1.0
|
||||
..badgeScale = (json['badgeScale'] as num?)?.toDouble() ?? 1.0
|
||||
..emoteScale = (json['emoteScale'] as num?)?.toDouble() ?? 1.0
|
||||
..emoteAutocomplete = json['emoteAutocomplete'] as bool? ?? true
|
||||
..sendCrashLogs = json['sendCrashLogs'] as bool? ?? true
|
||||
..fullScreen = json['fullScreen'] as bool? ?? false
|
||||
..expandInfo = json['expandInfo'] as bool? ?? true;
|
||||
|
|
@ -37,6 +41,7 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
<String, dynamic>{
|
||||
'themeType': _$ThemeTypeEnumMap[instance.themeType],
|
||||
'showThumbnailUptime': instance.showThumbnailUptime,
|
||||
'showThumbnails': instance.showThumbnails,
|
||||
'showVideo': instance.showVideo,
|
||||
'showOverlay': instance.showOverlay,
|
||||
'toggleableOverlay': instance.toggleableOverlay,
|
||||
|
|
@ -44,13 +49,15 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'showBottomBar': instance.showBottomBar,
|
||||
'showDeletedMessages': instance.showDeletedMessages,
|
||||
'showZeroWidth': instance.showZeroWidth,
|
||||
'showChatMessageDividers': instance.showChatMessageDividers,
|
||||
'timestampType': _$TimestampTypeEnumMap[instance.timestampType],
|
||||
'useReadableColors': instance.useReadableColors,
|
||||
'messageScale': instance.messageScale,
|
||||
'fontSize': instance.fontSize,
|
||||
'messageSpacing': instance.messageSpacing,
|
||||
'badgeHeight': instance.badgeHeight,
|
||||
'emoteHeight': instance.emoteHeight,
|
||||
'messageScale': instance.messageScale,
|
||||
'badgeScale': instance.badgeScale,
|
||||
'emoteScale': instance.emoteScale,
|
||||
'emoteAutocomplete': instance.emoteAutocomplete,
|
||||
'sendCrashLogs': instance.sendCrashLogs,
|
||||
'fullScreen': instance.fullScreen,
|
||||
'expandInfo': instance.expandInfo,
|
||||
|
|
@ -107,6 +114,21 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$showThumbnailsAtom = Atom(name: '_SettingsStoreBase.showThumbnails');
|
||||
|
||||
@override
|
||||
bool get showThumbnails {
|
||||
_$showThumbnailsAtom.reportRead();
|
||||
return super.showThumbnails;
|
||||
}
|
||||
|
||||
@override
|
||||
set showThumbnails(bool value) {
|
||||
_$showThumbnailsAtom.reportWrite(value, super.showThumbnails, () {
|
||||
super.showThumbnails = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$showVideoAtom = Atom(name: '_SettingsStoreBase.showVideo');
|
||||
|
||||
@override
|
||||
|
|
@ -215,6 +237,23 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$showChatMessageDividersAtom =
|
||||
Atom(name: '_SettingsStoreBase.showChatMessageDividers');
|
||||
|
||||
@override
|
||||
bool get showChatMessageDividers {
|
||||
_$showChatMessageDividersAtom.reportRead();
|
||||
return super.showChatMessageDividers;
|
||||
}
|
||||
|
||||
@override
|
||||
set showChatMessageDividers(bool value) {
|
||||
_$showChatMessageDividersAtom
|
||||
.reportWrite(value, super.showChatMessageDividers, () {
|
||||
super.showChatMessageDividers = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$timestampTypeAtom = Atom(name: '_SettingsStoreBase.timestampType');
|
||||
|
||||
@override
|
||||
|
|
@ -246,21 +285,6 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$messageScaleAtom = Atom(name: '_SettingsStoreBase.messageScale');
|
||||
|
||||
@override
|
||||
double get messageScale {
|
||||
_$messageScaleAtom.reportRead();
|
||||
return super.messageScale;
|
||||
}
|
||||
|
||||
@override
|
||||
set messageScale(double value) {
|
||||
_$messageScaleAtom.reportWrite(value, super.messageScale, () {
|
||||
super.messageScale = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$fontSizeAtom = Atom(name: '_SettingsStoreBase.fontSize');
|
||||
|
||||
@override
|
||||
|
|
@ -291,33 +315,64 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$badgeHeightAtom = Atom(name: '_SettingsStoreBase.badgeHeight');
|
||||
final _$messageScaleAtom = Atom(name: '_SettingsStoreBase.messageScale');
|
||||
|
||||
@override
|
||||
double get badgeHeight {
|
||||
_$badgeHeightAtom.reportRead();
|
||||
return super.badgeHeight;
|
||||
double get messageScale {
|
||||
_$messageScaleAtom.reportRead();
|
||||
return super.messageScale;
|
||||
}
|
||||
|
||||
@override
|
||||
set badgeHeight(double value) {
|
||||
_$badgeHeightAtom.reportWrite(value, super.badgeHeight, () {
|
||||
super.badgeHeight = value;
|
||||
set messageScale(double value) {
|
||||
_$messageScaleAtom.reportWrite(value, super.messageScale, () {
|
||||
super.messageScale = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$emoteHeightAtom = Atom(name: '_SettingsStoreBase.emoteHeight');
|
||||
final _$badgeScaleAtom = Atom(name: '_SettingsStoreBase.badgeScale');
|
||||
|
||||
@override
|
||||
double get emoteHeight {
|
||||
_$emoteHeightAtom.reportRead();
|
||||
return super.emoteHeight;
|
||||
double get badgeScale {
|
||||
_$badgeScaleAtom.reportRead();
|
||||
return super.badgeScale;
|
||||
}
|
||||
|
||||
@override
|
||||
set emoteHeight(double value) {
|
||||
_$emoteHeightAtom.reportWrite(value, super.emoteHeight, () {
|
||||
super.emoteHeight = value;
|
||||
set badgeScale(double value) {
|
||||
_$badgeScaleAtom.reportWrite(value, super.badgeScale, () {
|
||||
super.badgeScale = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$emoteScaleAtom = Atom(name: '_SettingsStoreBase.emoteScale');
|
||||
|
||||
@override
|
||||
double get emoteScale {
|
||||
_$emoteScaleAtom.reportRead();
|
||||
return super.emoteScale;
|
||||
}
|
||||
|
||||
@override
|
||||
set emoteScale(double value) {
|
||||
_$emoteScaleAtom.reportWrite(value, super.emoteScale, () {
|
||||
super.emoteScale = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$emoteAutocompleteAtom =
|
||||
Atom(name: '_SettingsStoreBase.emoteAutocomplete');
|
||||
|
||||
@override
|
||||
bool get emoteAutocomplete {
|
||||
_$emoteAutocompleteAtom.reportRead();
|
||||
return super.emoteAutocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
set emoteAutocomplete(bool value) {
|
||||
_$emoteAutocompleteAtom.reportWrite(value, super.emoteAutocomplete, () {
|
||||
super.emoteAutocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -366,11 +421,26 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$_SettingsStoreBaseActionController =
|
||||
ActionController(name: '_SettingsStoreBase');
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
final _$actionInfo = _$_SettingsStoreBaseActionController.startAction(
|
||||
name: '_SettingsStoreBase.reset');
|
||||
try {
|
||||
return super.reset();
|
||||
} finally {
|
||||
_$_SettingsStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
themeType: ${themeType},
|
||||
showThumbnailUptime: ${showThumbnailUptime},
|
||||
showThumbnails: ${showThumbnails},
|
||||
showVideo: ${showVideo},
|
||||
showOverlay: ${showOverlay},
|
||||
toggleableOverlay: ${toggleableOverlay},
|
||||
|
|
@ -378,13 +448,15 @@ pictureInPicture: ${pictureInPicture},
|
|||
showBottomBar: ${showBottomBar},
|
||||
showDeletedMessages: ${showDeletedMessages},
|
||||
showZeroWidth: ${showZeroWidth},
|
||||
showChatMessageDividers: ${showChatMessageDividers},
|
||||
timestampType: ${timestampType},
|
||||
useReadableColors: ${useReadableColors},
|
||||
messageScale: ${messageScale},
|
||||
fontSize: ${fontSize},
|
||||
messageSpacing: ${messageSpacing},
|
||||
badgeHeight: ${badgeHeight},
|
||||
emoteHeight: ${emoteHeight},
|
||||
messageScale: ${messageScale},
|
||||
badgeScale: ${badgeScale},
|
||||
emoteScale: ${emoteScale},
|
||||
emoteAutocomplete: ${emoteAutocomplete},
|
||||
sendCrashLogs: ${sendCrashLogs},
|
||||
fullScreen: ${fullScreen},
|
||||
expandInfo: ${expandInfo}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/widgets/block_button.dart';
|
||||
|
||||
class BlockedUsers extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const BlockedUsers({
|
||||
Key? key,
|
||||
required this.authStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Blocked Users'),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
await authStore.user.refreshBlockedUsers(headers: authStore.headersTwitch);
|
||||
},
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
if (authStore.user.blockedUsers.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('You don\'t have any blocked users.'),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
children: authStore.user.blockedUsers.map(
|
||||
(user) {
|
||||
final displayName = regexEnglish.hasMatch(user.displayName) ? user.displayName : user.displayName + ' (${user.userLogin})';
|
||||
return ListTile(
|
||||
title: Text(displayName),
|
||||
trailing: BlockButton(
|
||||
authStore: authStore,
|
||||
targetUser: displayName,
|
||||
targetUserId: user.userId,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
|
||||
class ProfileCard extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const ProfileCard({Key? key, required this.authStore}) : super(key: key);
|
||||
|
||||
Future<void> _showLoginDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: const Text('Log In With'),
|
||||
children: [
|
||||
ColoredBox(
|
||||
color: const Color.fromRGBO(145, 70, 255, 0.8),
|
||||
child: SimpleDialogOption(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: const Text(
|
||||
'Twitch Account',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
authStore.login();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
const Center(child: Text('Or')),
|
||||
SimpleDialogOption(
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Token',
|
||||
),
|
||||
onSubmitted: (token) {
|
||||
authStore.login(customToken: token);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showLogoutDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Log Out'),
|
||||
content: const Text('Are you sure you want to log out?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
authStore.logout();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
if (authStore.error != null) {
|
||||
return ListTile(
|
||||
title: const Text('Failed to connect'),
|
||||
trailing: OutlinedButton(
|
||||
onPressed: authStore.init,
|
||||
child: const Text('Try Again'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (authStore.isLoggedIn && authStore.user.details != null) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: const Color(0xFFFFFFFF),
|
||||
foregroundImage: CachedNetworkImageProvider(authStore.user.details!.profileImageUrl),
|
||||
),
|
||||
title: Text(authStore.user.details!.displayName),
|
||||
trailing: ElevatedButton.icon(
|
||||
onPressed: () => _showLogoutDialog(context),
|
||||
icon: const Icon(Icons.logout_outlined),
|
||||
label: const Text('Log Out'),
|
||||
style: ElevatedButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: const Icon(
|
||||
Icons.account_circle_outlined,
|
||||
size: 40,
|
||||
),
|
||||
title: const Text('Anonymous User'),
|
||||
subtitle: const Text('Log in to view your followed streams, send chat messages, and more.'),
|
||||
trailing: OutlinedButton(
|
||||
onPressed: () => _showLoginDialog(context),
|
||||
child: const Text('Log In'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,8 +32,9 @@ class BlockButton extends StatelessWidget {
|
|||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Cancel'),
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
TextButton(
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (isBlocked) {
|
||||
authStore.user.unblock(targetId: targetUserId, headers: authStore.headersTwitch);
|
||||
|
|
@ -47,7 +48,6 @@ class BlockButton extends StatelessWidget {
|
|||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class ScrollToTopButton extends StatelessWidget {
|
||||
final ScrollController scrollController;
|
||||
|
|
@ -21,7 +20,7 @@ class ScrollToTopButton extends StatelessWidget {
|
|||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
},
|
||||
child: const FaIcon(Icons.arrow_drop_up),
|
||||
child: const Icon(Icons.keyboard_arrow_up),
|
||||
mini: true,
|
||||
heroTag: null,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ class SectionHeader extends StatelessWidget {
|
|||
final String text;
|
||||
final EdgeInsets padding;
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
|
||||
const SectionHeader(
|
||||
this.text, {
|
||||
Key? key,
|
||||
this.padding = const EdgeInsets.fromLTRB(10.0, 30.0, 10.0, 5.0),
|
||||
this.padding = const EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 5.0),
|
||||
this.fontSize = 18.0,
|
||||
this.fontWeight = FontWeight.w600,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -20,7 +22,7 @@ class SectionHeader extends StatelessWidget {
|
|||
text,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: fontWeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
18
pubspec.lock
|
|
@ -7,21 +7,21 @@ packages:
|
|||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "37.0.0"
|
||||
version: "38.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
version: "3.4.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
version: "3.3.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -264,7 +264,7 @@ packages:
|
|||
name: flutter_native_splash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2+1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -337,7 +337,7 @@ packages:
|
|||
name: font_awesome_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
version: "10.1.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -764,7 +764,7 @@ packages:
|
|||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -846,7 +846,7 @@ packages:
|
|||
name: synchronized
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.0+2"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -986,7 +986,7 @@ packages:
|
|||
name: webview_flutter_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.3"
|
||||
version: "2.8.4"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1007,7 +1007,7 @@ packages:
|
|||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.5.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.1.0+17
|
||||
version: 1.2.0+18
|
||||
|
||||
environment:
|
||||
sdk: ">=2.15.0 <3.0.0"
|
||||
|
|
@ -29,7 +29,7 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
json_annotation: ^4.3.0
|
||||
json_annotation: ^4.4.0
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
|
|
@ -99,7 +99,8 @@ flutter:
|
|||
# - images/a_dot_ham.jpeg
|
||||
|
||||
assets:
|
||||
- assets/icons/logo.png
|
||||
- assets/icons/logo.svg
|
||||
- assets/icons/TwitchGlitchWhite.png
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<picture id="splash">
|
||||
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light) or (prefers-color-scheme: no-preference)">
|
||||
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)">
|
||||
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
||||
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" />
|
||||
</picture>
|
||||
|
|
|
|||