update styles

This commit is contained in:
Tommy Chow 2025-06-25 21:56:33 -04:00
parent 56961c7010
commit 2008d2c688
13 changed files with 217 additions and 262 deletions

View file

@ -5,11 +5,12 @@ import 'package:frosty/screens/home/home_store.dart';
import 'package:frosty/screens/home/search/search.dart';
import 'package:frosty/screens/home/stream_list/stream_list_store.dart';
import 'package:frosty/screens/home/stream_list/streams_list.dart';
import 'package:frosty/screens/home/top/top.dart';
import 'package:frosty/screens/home/top/categories/categories.dart';
import 'package:frosty/screens/settings/settings.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/screens/settings/widgets/release_notes.dart';
import 'package:frosty/widgets/app_bar.dart';
import 'package:frosty/widgets/profile_picture.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
@ -63,13 +64,15 @@ class _HomeState extends State<Home> {
return GestureDetector(
onTap: FocusScope.of(context).unfocus,
child: Scaffold(
appBar: AppBar(
shape: const Border(),
extendBodyBehindAppBar: true,
appBar: FrostyAppBar(
showBackButton: false,
title: Observer(
builder: (_) {
final titles = [
if (_authStore.isLoggedIn) 'Following',
'Top',
'Top Streams',
'Top Categories',
'Search',
];
@ -99,6 +102,7 @@ class _HomeState extends State<Home> {
],
),
body: SafeArea(
top: false,
child: Observer(
builder: (_) => IndexedStack(
index: _homeStore.selectedIndex,
@ -107,9 +111,15 @@ class _HomeState extends State<Home> {
StreamsList(
listType: ListType.followed,
scrollController: _homeStore.followedScrollController,
addTopPadding: true,
),
TopSection(
homeStore: _homeStore,
StreamsList(
listType: ListType.top,
scrollController: _homeStore.topSectionScrollControllers[0],
addTopPadding: true,
),
Categories(
scrollController: _homeStore.topSectionScrollControllers[1],
),
Search(
scrollController: _homeStore.searchScrollController,
@ -139,8 +149,16 @@ class _HomeState extends State<Home> {
Icons.arrow_upward_rounded,
),
selectedIcon: Icon(Icons.arrow_upward_rounded),
label: 'Top',
tooltip: 'Top',
label: 'Top streams',
tooltip: 'Top streams',
),
const NavigationDestination(
icon: Icon(
Icons.gamepad_outlined,
),
selectedIcon: Icon(Icons.gamepad_rounded),
label: 'Top categories',
tooltip: 'Top categories',
),
const NavigationDestination(
icon: Icon(

View file

@ -33,126 +33,133 @@ class _SearchState extends State<Search> {
@override
Widget build(BuildContext context) {
return Column(
children: [
Observer(
builder: (context) {
return Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchStore.textEditingController,
focusNode: _searchStore.textFieldFocusNode,
autocorrect: false,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search_rounded),
hintText: 'Find a channel or category',
suffixIcon: _searchStore.textFieldFocusNode.hasFocus ||
_searchStore.searchText.isNotEmpty
? IconButton(
icon: const Icon(Icons.close_rounded),
tooltip: _searchStore.searchText.isEmpty
? 'Cancel'
: 'Clear',
onPressed: () {
if (_searchStore.searchText.isEmpty) {
_searchStore.textFieldFocusNode.unfocus();
}
_searchStore.textEditingController.clear();
},
)
: null,
return SafeArea(
child: Column(
children: [
Observer(
builder: (context) {
return Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchStore.textEditingController,
focusNode: _searchStore.textFieldFocusNode,
autocorrect: false,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search_rounded),
hintText: 'Find a channel or category',
suffixIcon: _searchStore.textFieldFocusNode.hasFocus ||
_searchStore.searchText.isNotEmpty
? IconButton(
icon: const Icon(Icons.close_rounded),
tooltip: _searchStore.searchText.isEmpty
? 'Cancel'
: 'Clear',
onPressed: () {
if (_searchStore.searchText.isEmpty) {
_searchStore.textFieldFocusNode.unfocus();
}
_searchStore.textEditingController.clear();
},
)
: null,
),
onSubmitted: _searchStore.handleQuery,
),
onSubmitted: _searchStore.handleQuery,
),
);
},
),
AnimatedScrollBorder(scrollController: widget.scrollController),
Expanded(
child: Scrollbar(
controller: widget.scrollController,
child: Observer(
builder: (context) {
if (_searchStore.textEditingController.text.isEmpty) {
if (_searchStore.searchHistory.isEmpty) {
return const AlertMessage(message: 'No recent searches');
}
);
},
),
AnimatedScrollBorder(scrollController: widget.scrollController),
Expanded(
child: Scrollbar(
controller: widget.scrollController,
child: Observer(
builder: (context) {
if (_searchStore.textEditingController.text.isEmpty) {
if (_searchStore.searchHistory.isEmpty) {
return const AlertMessage(
message: 'No recent searches',
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SectionHeader(
'History',
padding: EdgeInsets.zero,
isFirst: true,
),
TextButton(
onPressed: _searchStore.searchHistory.clear,
child: const Text('Clear'),
),
],
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SectionHeader(
'History',
padding: EdgeInsets.zero,
isFirst: true,
),
TextButton(
onPressed: _searchStore.searchHistory.clear,
child: const Text('Clear'),
),
],
),
),
Expanded(
child: ListView(
controller: widget.scrollController,
children: _searchStore.searchHistory
.mapIndexed(
(index, searchTerm) => ListTile(
leading: const Icon(Icons.history_rounded),
title: Text(searchTerm),
onTap: () {
_searchStore.textEditingController.text =
searchTerm;
_searchStore.handleQuery(searchTerm);
_searchStore
.textEditingController.selection =
TextSelection.fromPosition(
TextPosition(
offset: _searchStore
.textEditingController
.text
.length,
),
);
},
),
)
.toList(),
),
),
],
);
}
return CustomScrollView(
controller: widget.scrollController,
slivers: [
const SliverToBoxAdapter(
child: SectionHeader(
'Channels',
isFirst: true,
padding: EdgeInsets.fromLTRB(16, 12, 16, 8),
),
),
Expanded(
child: ListView(
controller: widget.scrollController,
children: _searchStore.searchHistory
.mapIndexed(
(index, searchTerm) => ListTile(
leading: const Icon(Icons.history_rounded),
title: Text(searchTerm),
onTap: () {
_searchStore.textEditingController.text =
searchTerm;
_searchStore.handleQuery(searchTerm);
_searchStore.textEditingController
.selection = TextSelection.fromPosition(
TextPosition(
offset: _searchStore
.textEditingController.text.length,
),
);
},
),
)
.toList(),
SearchResultsChannels(
searchStore: _searchStore,
query: _searchStore.searchText,
),
const SliverToBoxAdapter(
child: SectionHeader(
'Categories',
),
),
SearchResultsCategories(searchStore: _searchStore),
],
);
}
return CustomScrollView(
controller: widget.scrollController,
slivers: [
const SliverToBoxAdapter(
child: SectionHeader(
'Channels',
isFirst: true,
padding: EdgeInsets.fromLTRB(16, 12, 16, 8),
),
),
SearchResultsChannels(
searchStore: _searchStore,
query: _searchStore.searchText,
),
const SliverToBoxAdapter(
child: SectionHeader(
'Categories',
),
),
SearchResultsCategories(searchStore: _searchStore),
],
);
},
},
),
),
),
),
],
],
),
);
}

View file

@ -9,7 +9,6 @@ import 'package:frosty/screens/home/top/categories/category_card.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/widgets/alert_message.dart';
import 'package:frosty/widgets/animated_scroll_border.dart';
import 'package:frosty/widgets/loading_indicator.dart';
import 'package:frosty/widgets/scroll_to_top_button.dart';
import 'package:frosty/widgets/section_header.dart';
@ -27,6 +26,7 @@ class StreamsList extends StatefulWidget {
final ScrollController? scrollController;
final bool showJumpButton;
final bool addTopPadding;
const StreamsList({
super.key,
@ -34,6 +34,7 @@ class StreamsList extends StatefulWidget {
this.categoryId,
this.scrollController,
this.showJumpButton = false,
this.addTopPadding = false,
});
@override
@ -74,9 +75,13 @@ class _StreamsListState extends State<StreamsList>
Widget build(BuildContext context) {
super.build(context);
final double topPadding =
widget.addTopPadding ? MediaQuery.of(context).padding.top : 0;
_listStore.checkLastTimeRefreshedAndUpdate();
return RefreshIndicator.adaptive(
edgeOffset: topPadding,
onRefresh: () async {
HapticFeedback.lightImpact();
@ -152,10 +157,6 @@ class _StreamsListState extends State<StreamsList>
category: _listStore.categoryDetails!,
isTappable: false,
),
if (_listStore.scrollController != null)
AnimatedScrollBorder(
scrollController: _listStore.scrollController!,
),
Expanded(
child: Scrollbar(
controller: _listStore.scrollController,
@ -163,6 +164,12 @@ class _StreamsListState extends State<StreamsList>
physics: const AlwaysScrollableScrollPhysics(),
controller: _listStore.scrollController,
slivers: [
if (widget.addTopPadding)
SliverPadding(
padding: EdgeInsets.only(
top: topPadding,
),
),
if (isFollowingTab &&
_listStore.pinnedStreams.isNotEmpty) ...[
const SliverToBoxAdapter(

View file

@ -54,6 +54,7 @@ class _CategoriesState extends State<Categories>
_categoriesStore.checkLastTimeRefreshedAndUpdate();
return RefreshIndicator.adaptive(
edgeOffset: MediaQuery.of(context).padding.top,
onRefresh: () async {
HapticFeedback.lightImpact();
await _categoriesStore.refreshCategories();

View file

@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:frosty/screens/home/home_store.dart';
import 'package:frosty/screens/home/stream_list/stream_list_store.dart';
import 'package:frosty/screens/home/stream_list/streams_list.dart';
import 'package:frosty/screens/home/top/categories/categories.dart';
class TopSection extends StatefulWidget {
final HomeStore homeStore;
const TopSection({
super.key,
required this.homeStore,
});
@override
State<TopSection> createState() => _TopSectionState();
}
class _TopSectionState extends State<TopSection>
with SingleTickerProviderStateMixin {
late final _tabBarController = TabController(length: 2, vsync: this);
@override
void initState() {
super.initState();
_tabBarController.addListener(
() => widget.homeStore.topSectionCurrentIndex = _tabBarController.index,
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
isScrollable: true,
controller: _tabBarController,
tabs: const [
Tab(
child: Text('Streams'),
),
Tab(child: Text('Categories')),
],
),
Expanded(
child: TabBarView(
controller: _tabBarController,
children: [
StreamsList(
listType: ListType.top,
scrollController:
widget.homeStore.topSectionScrollControllers[0],
),
Categories(
scrollController:
widget.homeStore.topSectionScrollControllers[1],
),
],
),
),
],
);
}
@override
void dispose() {
_tabBarController.dispose();
super.dispose();
}
}

View file

@ -31,7 +31,6 @@ class _ChatSettingsState extends State<ChatSettings> {
return Observer(
builder: (context) => ListView(
padding: const EdgeInsets.only(top: 16),
children: [
const SectionHeader(
'Message sizing',

View file

@ -16,7 +16,6 @@ class GeneralSettings extends StatelessWidget {
Widget build(BuildContext context) {
return Observer(
builder: (context) => ListView(
padding: const EdgeInsets.only(top: 16),
children: [
const SectionHeader(
'Theme',

View file

@ -23,6 +23,7 @@ class Settings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: FrostyAppBar(
title: const Text('Settings'),
actions: [
@ -53,12 +54,12 @@ class Settings extends StatelessWidget {
],
),
body: SafeArea(
top: false,
bottom: false,
child: ListView(
padding: const EdgeInsets.only(top: 16),
children: [
const SectionHeader(
'Profile',
'Account',
isFirst: true,
),
ProfileCard(authStore: context.read<AuthStore>()),

View file

@ -17,7 +17,6 @@ class VideoSettings extends StatelessWidget {
Widget build(BuildContext context) {
return Observer(
builder: (context) => ListView(
padding: const EdgeInsets.only(top: 16),
children: [
const SectionHeader(
'Player',
@ -34,7 +33,7 @@ class VideoSettings extends StatelessWidget {
value: settingsStore.defaultToHighestQuality,
onChanged: (newValue) =>
settingsStore.defaultToHighestQuality = newValue,
),
),
if (Platform.isAndroid)
SettingsListSwitch(
title: 'Use enhanced rendering',

View file

@ -25,8 +25,10 @@ class SettingsTileRoute extends StatelessWidget {
MaterialPageRoute(
builder: (context) => useScaffold
? Scaffold(
extendBodyBehindAppBar: true,
appBar: FrostyAppBar(title: Text(title)),
body: SafeArea(
top: false,
bottom: false,
child: child,
),

View file

@ -7,24 +7,29 @@ class FrostyThemes {
const FrostyThemes({required this.colorSchemeSeed});
ThemeData get light => createBaseTheme(
colorScheme: ColorScheme.fromSeed(seedColor: colorSchemeSeed),
backgroundColor: const Color.fromRGBO(248, 248, 248, 1),
);
ThemeData get dark => createBaseTheme(
colorScheme: ColorScheme.fromSeed(
seedColor: colorSchemeSeed,
brightness: Brightness.dark,
),
backgroundColor: Colors.black,
);
ThemeData createBaseTheme({
required Brightness brightness,
required Color colorSchemeSeed,
required ColorScheme colorScheme,
Color? backgroundColor,
}) {
final colorScheme = ColorScheme.fromSeed(
seedColor: colorSchemeSeed,
brightness: brightness,
);
final borderColor = colorScheme.outlineVariant;
const borderWidth = 0.5;
return ThemeData(
fontFamily: 'Inter',
brightness: brightness,
colorScheme: colorScheme,
fontFamily: 'Inter',
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
scaffoldBackgroundColor: backgroundColor,
bottomSheetTheme: BottomSheetThemeData(
@ -37,11 +42,14 @@ class FrostyThemes {
surfaceTintColor: backgroundColor,
),
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: backgroundColor,
backgroundColor: Colors.transparent,
surfaceTintColor: backgroundColor,
shape: Border(
bottom: BorderSide(color: borderColor, width: borderWidth),
),
listTileTheme: ListTileThemeData(
titleTextStyle: TextStyle(
color: colorScheme.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
inputDecorationTheme: InputDecorationTheme(
@ -61,7 +69,6 @@ class FrostyThemes {
),
),
navigationBarTheme: NavigationBarThemeData(
elevation: 0,
backgroundColor: backgroundColor,
height: 64,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
@ -71,6 +78,7 @@ class FrostyThemes {
tabAlignment: TabAlignment.start,
),
tooltipTheme: TooltipThemeData(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(8)),
@ -133,24 +141,4 @@ class FrostyThemes {
),
);
}
ThemeData get light {
final theme = createBaseTheme(
brightness: Brightness.light,
colorSchemeSeed: colorSchemeSeed,
backgroundColor: const Color.fromRGBO(248, 248, 248, 1),
);
return theme;
}
ThemeData get dark {
final theme = createBaseTheme(
brightness: Brightness.dark,
colorSchemeSeed: colorSchemeSeed,
backgroundColor: Colors.black,
);
return theme;
}
}

View file

@ -1,26 +1,37 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class FrostyAppBar extends StatelessWidget implements PreferredSizeWidget {
final Widget title;
final Widget? title;
final List<Widget>? actions;
final bool showBackButton;
const FrostyAppBar({
super.key,
required this.title,
this.title,
this.actions,
this.showBackButton = true,
});
@override
Widget build(BuildContext context) {
return AppBar(
leading: IconButton(
tooltip: 'Back',
icon: Icon(Icons.adaptive.arrow_back_rounded),
onPressed: Navigator.of(context).pop,
return ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: AppBar(
leading: showBackButton
? IconButton(
tooltip: 'Back',
icon: Icon(Icons.adaptive.arrow_back_rounded),
onPressed: Navigator.of(context).pop,
)
: null,
title: title,
actions: actions,
centerTitle: false,
),
),
title: title,
actions: actions,
centerTitle: false,
);
}

View file

@ -17,23 +17,17 @@ class SectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (!isFirst) ...[
const Divider(),
const SizedBox(height: 16),
],
Text(
text,
style: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: FontWeight.w600,
),
),
],
padding: padding ??
(isFirst
? const EdgeInsets.fromLTRB(16, 16, 16, 8)
: const EdgeInsets.fromLTRB(16, 48, 16, 8)),
child: Text(
text.toUpperCase(),
style: TextStyle(
fontSize: fontSize ?? 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
);
}