blob: 80f2ea136327ed30b29aeee03c89d3f5c6a969d9 [file] [log] [blame]
// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/gallery_localizations.dart';
import 'package:gallery/data/gallery_options.dart';
import 'package:gallery/layout/image_placeholder.dart';
import 'package:gallery/layout/text_scale.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
class ArticleData {
ArticleData({
required this.imageUrl,
required this.imageAspectRatio,
required this.category,
required this.title,
this.snippet,
});
final String imageUrl;
final double imageAspectRatio;
final String category;
final String title;
final String? snippet;
}
class HorizontalArticlePreview extends StatelessWidget {
const HorizontalArticlePreview({
super.key,
required this.data,
this.minutes,
});
final ArticleData data;
final int? minutes;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
data.category,
style: textTheme.titleMedium,
),
const SizedBox(height: 12),
SelectableText(
data.title,
style: textTheme.headlineSmall!.copyWith(fontSize: 16),
),
],
),
),
if (minutes != null) ...[
SelectableText(
GalleryLocalizations.of(context)!.craneMinutes(minutes!),
style: textTheme.bodyLarge,
),
const SizedBox(width: 8),
],
FadeInImagePlaceholder(
image: AssetImage(data.imageUrl, package: 'flutter_gallery_assets'),
placeholder: Container(
color: Colors.black.withOpacity(0.1),
width: 64 / (1 / data.imageAspectRatio),
height: 64,
),
fit: BoxFit.cover,
excludeFromSemantics: true,
),
],
);
}
}
class VerticalArticlePreview extends StatelessWidget {
const VerticalArticlePreview({
super.key,
required this.data,
this.width,
this.headlineTextStyle,
this.showSnippet = false,
});
final ArticleData data;
final double? width;
final TextStyle? headlineTextStyle;
final bool showSnippet;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return SizedBox(
width: width ?? double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: double.infinity,
child: FadeInImagePlaceholder(
image: AssetImage(
data.imageUrl,
package: 'flutter_gallery_assets',
),
placeholder: LayoutBuilder(builder: (context, constraints) {
return Container(
color: Colors.black.withOpacity(0.1),
width: constraints.maxWidth,
height: constraints.maxWidth / data.imageAspectRatio,
);
}),
fit: BoxFit.fitWidth,
width: double.infinity,
excludeFromSemantics: true,
),
),
const SizedBox(height: 12),
SelectableText(
data.category,
style: textTheme.titleMedium,
),
const SizedBox(height: 12),
SelectableText(
data.title,
style: headlineTextStyle ?? textTheme.headlineSmall,
),
if (showSnippet) ...[
const SizedBox(height: 4),
SelectableText(
data.snippet!,
style: textTheme.bodyMedium,
),
],
],
),
);
}
}
List<Widget> buildArticlePreviewItems(BuildContext context) {
final localizations = GalleryLocalizations.of(context)!;
Widget articleDivider = Container(
margin: const EdgeInsets.symmetric(vertical: 16),
color: Colors.black.withOpacity(0.07),
height: 1,
);
Widget sectionDivider = Container(
margin: const EdgeInsets.symmetric(vertical: 16),
color: Colors.black.withOpacity(0.2),
height: 1,
);
final textTheme = Theme.of(context).textTheme;
return <Widget>[
VerticalArticlePreview(
data: ArticleData(
imageUrl: 'fortnightly/fortnightly_healthcare.jpg',
imageAspectRatio: 391 / 248,
category: localizations.fortnightlyMenuWorld.toUpperCase(),
title: localizations.fortnightlyHeadlineHealthcare,
),
headlineTextStyle: textTheme.headlineSmall!.copyWith(fontSize: 20),
),
articleDivider,
HorizontalArticlePreview(
data: ArticleData(
imageUrl: 'fortnightly/fortnightly_war.png',
imageAspectRatio: 1,
category: localizations.fortnightlyMenuPolitics.toUpperCase(),
title: localizations.fortnightlyHeadlineWar,
),
),
articleDivider,
HorizontalArticlePreview(
data: ArticleData(
imageUrl: 'fortnightly/fortnightly_gas.png',
imageAspectRatio: 1,
category: localizations.fortnightlyMenuTech.toUpperCase(),
title: localizations.fortnightlyHeadlineGasoline,
),
),
sectionDivider,
SelectableText(
localizations.fortnightlyLatestUpdates,
style: textTheme.titleLarge,
),
articleDivider,
HorizontalArticlePreview(
data: ArticleData(
imageUrl: 'fortnightly/fortnightly_army.png',
imageAspectRatio: 1,
category: localizations.fortnightlyMenuPolitics.toUpperCase(),
title: localizations.fortnightlyHeadlineArmy,
),
minutes: 2,
),
articleDivider,
HorizontalArticlePreview(
data: ArticleData(
imageUrl: 'fortnightly/fortnightly_stocks.png',
imageAspectRatio: 77 / 64,
category: localizations.fortnightlyMenuWorld.toUpperCase(),
title: localizations.fortnightlyHeadlineStocks,
),
minutes: 5,
),
articleDivider,
HorizontalArticlePreview(
data: ArticleData(
imageUrl: 'fortnightly/fortnightly_fabrics.png',
imageAspectRatio: 76 / 64,
category: localizations.fortnightlyMenuTech.toUpperCase(),
title: localizations.fortnightlyHeadlineFabrics,
),
minutes: 4,
),
articleDivider,
];
}
class HashtagBar extends StatelessWidget {
const HashtagBar({super.key});
@override
Widget build(BuildContext context) {
final verticalDivider = Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.black.withOpacity(0.1),
width: 1,
);
final textTheme = Theme.of(context).textTheme;
final height = 32 * reducedTextScale(context);
final localizations = GalleryLocalizations.of(context)!;
return SizedBox(
height: height,
child: ListView(
restorationId: 'hashtag_bar_list_view',
scrollDirection: Axis.horizontal,
children: [
const SizedBox(width: 16),
Center(
child: SelectableText(
'#${localizations.fortnightlyTrendingTechDesign}',
style: textTheme.titleSmall,
),
),
verticalDivider,
Center(
child: SelectableText(
'#${localizations.fortnightlyTrendingReform}',
style: textTheme.titleSmall,
),
),
verticalDivider,
Center(
child: SelectableText(
'#${localizations.fortnightlyTrendingHealthcareRevolution}',
style: textTheme.titleSmall,
),
),
verticalDivider,
Center(
child: SelectableText(
'#${localizations.fortnightlyTrendingGreenArmy}',
style: textTheme.titleSmall,
),
),
verticalDivider,
Center(
child: SelectableText(
'#${localizations.fortnightlyTrendingStocks}',
style: textTheme.titleSmall,
),
),
verticalDivider,
],
),
);
}
}
class NavigationMenu extends StatelessWidget {
const NavigationMenu({super.key, this.isCloseable = false});
final bool isCloseable;
@override
Widget build(BuildContext context) {
final localizations = GalleryLocalizations.of(context)!;
return ListView(
children: [
if (isCloseable)
Row(
children: [
IconButton(
icon: const Icon(Icons.close),
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
onPressed: () => Navigator.pop(context),
),
Image.asset(
'fortnightly/fortnightly_title.png',
package: 'flutter_gallery_assets',
excludeFromSemantics: true,
),
],
),
const SizedBox(height: 32),
MenuItem(
localizations.fortnightlyMenuFrontPage,
header: true,
),
MenuItem(localizations.fortnightlyMenuWorld),
MenuItem(localizations.fortnightlyMenuUS),
MenuItem(localizations.fortnightlyMenuPolitics),
MenuItem(localizations.fortnightlyMenuBusiness),
MenuItem(localizations.fortnightlyMenuTech),
MenuItem(localizations.fortnightlyMenuScience),
MenuItem(localizations.fortnightlyMenuSports),
MenuItem(localizations.fortnightlyMenuTravel),
MenuItem(localizations.fortnightlyMenuCulture),
],
);
}
}
class MenuItem extends StatelessWidget {
const MenuItem(this.title, {super.key, this.header = false});
final String title;
final bool header;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 32,
alignment: Alignment.centerLeft,
child: header ? null : const Icon(Icons.arrow_drop_down),
),
Expanded(
child: SelectableText(
title,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: header ? FontWeight.w700 : FontWeight.w600,
fontSize: 16,
),
),
),
],
),
);
}
}
class StockItem extends StatelessWidget {
const StockItem({
super.key,
required this.ticker,
required this.price,
required this.percent,
});
final String ticker;
final String price;
final double percent;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final percentFormat = NumberFormat.decimalPercentPattern(
locale: GalleryOptions.of(context).locale.toString(),
decimalDigits: 2,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(ticker, style: textTheme.titleMedium),
const SizedBox(height: 2),
Row(
children: [
Expanded(
child: SelectableText(
price,
style: textTheme.titleSmall!.copyWith(
color: textTheme.titleSmall!.color!.withOpacity(0.75),
),
),
),
SelectableText(
percent > 0 ? '+' : '-',
style: textTheme.titleSmall!.copyWith(
fontSize: 12,
color: percent > 0
? const Color(0xff20CF63)
: const Color(0xff661FFF),
),
),
const SizedBox(width: 4),
SelectableText(
percentFormat.format(percent.abs() / 100),
style: textTheme.bodySmall!.copyWith(
fontSize: 12,
color: textTheme.titleSmall!.color!.withOpacity(0.75),
),
),
],
)
],
);
}
}
List<Widget> buildStockItems(BuildContext context) {
Widget articleDivider = Container(
margin: const EdgeInsets.symmetric(vertical: 16),
color: Colors.black.withOpacity(0.07),
height: 1,
);
const imageAspectRatio = 165 / 55;
return <Widget>[
SizedBox(
width: double.infinity,
child: FadeInImagePlaceholder(
image: const AssetImage(
'fortnightly/fortnightly_chart.png',
package: 'flutter_gallery_assets',
),
placeholder: LayoutBuilder(builder: (context, constraints) {
return Container(
color: Colors.black.withOpacity(0.1),
width: constraints.maxWidth,
height: constraints.maxWidth / imageAspectRatio,
);
}),
width: double.infinity,
fit: BoxFit.contain,
excludeFromSemantics: true,
),
),
articleDivider,
const StockItem(
ticker: 'DIJA',
price: '7,031.21',
percent: -0.48,
),
articleDivider,
const StockItem(
ticker: 'SP',
price: '1,967.84',
percent: -0.23,
),
articleDivider,
const StockItem(
ticker: 'Nasdaq',
price: '6,211.46',
percent: 0.52,
),
articleDivider,
const StockItem(
ticker: 'Nikkei',
price: '5,891',
percent: 1.16,
),
articleDivider,
const StockItem(
ticker: 'DJ Total',
price: '89.02',
percent: 0.80,
),
articleDivider,
];
}
class VideoPreview extends StatelessWidget {
const VideoPreview({
super.key,
required this.data,
required this.time,
});
final ArticleData data;
final String time;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: double.infinity,
child: FadeInImagePlaceholder(
image: AssetImage(
data.imageUrl,
package: 'flutter_gallery_assets',
),
placeholder: LayoutBuilder(builder: (context, constraints) {
return Container(
color: Colors.black.withOpacity(0.1),
width: constraints.maxWidth,
height: constraints.maxWidth / data.imageAspectRatio,
);
}),
fit: BoxFit.contain,
width: double.infinity,
excludeFromSemantics: true,
),
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: SelectableText(
data.category,
style: textTheme.titleMedium,
),
),
SelectableText(time, style: textTheme.bodyLarge)
],
),
const SizedBox(height: 4),
SelectableText(data.title,
style: textTheme.headlineSmall!.copyWith(fontSize: 16)),
],
);
}
}
List<Widget> buildVideoPreviewItems(BuildContext context) {
final localizations = GalleryLocalizations.of(context)!;
return <Widget>[
VideoPreview(
data: ArticleData(
imageUrl: 'fortnightly/fortnightly_feminists.jpg',
imageAspectRatio: 148 / 88,
category: localizations.fortnightlyMenuPolitics.toUpperCase(),
title: localizations.fortnightlyHeadlineFeminists,
),
time: '2:31',
),
const SizedBox(height: 32),
VideoPreview(
data: ArticleData(
imageUrl: 'fortnightly/fortnightly_bees.jpg',
imageAspectRatio: 148 / 88,
category: localizations.fortnightlyMenuUS.toUpperCase(),
title: localizations.fortnightlyHeadlineBees,
),
time: '1:37',
),
];
}
ThemeData buildTheme(BuildContext context) {
final lightTextTheme = ThemeData.light().textTheme;
return ThemeData(
scaffoldBackgroundColor: Colors.white,
appBarTheme: AppBarTheme(
color: Colors.white,
elevation: 0,
iconTheme: IconTheme.of(context).copyWith(color: Colors.black),
),
highlightColor: Colors.transparent,
textTheme: TextTheme(
// preview snippet
bodyMedium: GoogleFonts.merriweather(
fontWeight: FontWeight.w300,
fontSize: 16,
textStyle: lightTextTheme.bodyMedium,
),
// time in latest updates
bodyLarge: GoogleFonts.libreFranklin(
fontWeight: FontWeight.w500,
fontSize: 11,
color: Colors.black.withOpacity(0.5),
textStyle: lightTextTheme.bodyLarge,
),
// preview headlines
headlineSmall: GoogleFonts.libreFranklin(
fontWeight: FontWeight.w500,
fontSize: 16,
textStyle: lightTextTheme.headlineSmall,
),
// (caption 2), preview category, stock ticker
titleMedium: GoogleFonts.robotoCondensed(
fontWeight: FontWeight.w700,
fontSize: 16,
),
titleSmall: GoogleFonts.libreFranklin(
fontWeight: FontWeight.w400,
fontSize: 14,
textStyle: lightTextTheme.titleSmall,
),
// section titles: Top Highlights, Last Updated...
titleLarge: GoogleFonts.merriweather(
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
fontSize: 14,
textStyle: lightTextTheme.titleLarge,
),
),
);
}