Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 28 additions & 23 deletions lib/app/new_home/_widgets/assistant_hint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,35 @@ class AssistantHint extends StatelessWidget {
builder: (context, data, _) {
final text = data != null ? _buildHintText(data.$1, data.$2) : '載入天氣資料中…';

return Padding(
padding: const .symmetric(horizontal: 12, vertical: 8),
child: Card(
child: Padding(
padding: const .all(12),
child: Row(
spacing: 8,
crossAxisAlignment: .start,
children: [
Icon(
Symbols.auto_awesome_rounded,
fill: 1,
color: context.colors.onSurfaceVariant,
),
Expanded(
child: Text(
text,
style: TextStyle(fontSize: 16, color: context.colors.onSurfaceVariant),
maxLines: 2,
overflow: .ellipsis,
),
return Container(
margin: const .symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: context.colors.surfaceContainerLow,
borderRadius: .circular(16),
border: Border.all(
color: context.colors.outlineVariant.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const .all(12),
child: Row(
spacing: 8,
crossAxisAlignment: .start,
children: [
Icon(
Symbols.auto_awesome_rounded,
fill: 1,
color: context.colors.onSurfaceVariant,
),
Expanded(
child: Text(
text,
style: TextStyle(fontSize: 16, color: context.colors.onSurfaceVariant),
maxLines: 2,
overflow: .ellipsis,
),
],
),
),
],
),
),
);
Expand Down
61 changes: 33 additions & 28 deletions lib/app/new_home/_widgets/day_cycle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,39 @@ class DayCycle extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Padding(
padding: const .symmetric(horizontal: 12, vertical: 4),
child: Card(
child: Padding(
padding: const .all(16),
child: Column(
crossAxisAlignment: .start,
spacing: 8,
children: [
Row(
spacing: 4,
children: [
const Icon(
Symbols.wb_twilight_rounded,
fill: 1,
color: Colors.orangeAccent,
),
BodyText.large('日出日落', weight: .bold),
],
),
const SizedBox(height: 16),
_SunCycleGraph(
sunrise: const TimeOfDay(hour: 5, minute: 30),
sunset: const TimeOfDay(hour: 18, minute: 30),
now: TimeOfDay.now(),
),
],
),
return Container(
margin: const .symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: context.colors.surfaceContainerLow,
borderRadius: .circular(16),
border: Border.all(
color: context.colors.outlineVariant.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const .all(16),
child: Column(
crossAxisAlignment: .start,
spacing: 8,
children: [
Row(
spacing: 4,
children: [
const Icon(
Symbols.wb_twilight_rounded,
fill: 1,
color: Colors.orangeAccent,
),
BodyText.large('日出日落', weight: .bold),
],
),
const SizedBox(height: 16),
_SunCycleGraph(
sunrise: const TimeOfDay(hour: 5, minute: 30),
sunset: const TimeOfDay(hour: 18, minute: 30),
now: TimeOfDay.now(),
),
],
),
),
);
Expand Down
169 changes: 169 additions & 0 deletions lib/app/new_home/_widgets/forecast.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/// 24 小時預報。
library;

import 'package:dpip/app/new_home/_models/home_model.dart';
import 'package:dpip/core/i18n.dart';
import 'package:dpip/utils/extensions/build_context.dart';
import 'package:dpip/utils/log.dart';
import 'package:dpip/widgets/responsive/responsive_container.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';

const double _kColumnWidth = 56.0;

class Forecast extends StatelessWidget {
const Forecast({super.key});

@override
Widget build(BuildContext context) {
return Selector<HomeModel, Map<String, dynamic>?>(
selector: (_, m) => m.forecast,
builder: (context, forecast, _) {
if (forecast == null) return const SizedBox.shrink();
try {
final data = forecast['forecast'] as List<dynamic>?;
if (data == null || data.isEmpty) return const SizedBox.shrink();

return ResponsiveContainer(
child: Container(
margin: const .symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: context.colors.surfaceContainerLow,
borderRadius: .circular(16),
border: Border.all(
color: context.colors.outlineVariant.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const .symmetric(vertical: 14),
child: Column(
crossAxisAlignment: .start,
children: [
Padding(
padding: const .symmetric(horizontal: 14),
child: Text(
'24 小時預報'.i18n,
style: context.texts.labelMedium?.copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: .w600,
letterSpacing: 0.5,
),
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: .horizontal,
padding: const .symmetric(horizontal: 6),
child: Row(
crossAxisAlignment: .start,
children: [
for (var i = 0; i < data.length; i++)
_HourColumn(
item: data[i] as Map<String, dynamic>,
isNow: i == 0,
),
],
),
),
],
),
),
),
);
} catch (e, s) {
TalkerManager.instance.error('Failed to render forecast card', e, s);
}
return const SizedBox.shrink();
},
);
}
}

class _HourColumn extends StatelessWidget {
final Map<String, dynamic> item;
final bool isNow;

const _HourColumn({required this.item, required this.isNow});

static (IconData, Color?) _weatherIcon(BuildContext context, String weather) => switch (weather) {
final s when s.contains('晴') => (Symbols.sunny_rounded, Colors.orangeAccent),
final s when s.contains('雨') => (Symbols.rainy_light_rounded, Colors.blueAccent),
final s when s.contains('雲') || s.contains('陰') => (
Symbols.cloud_rounded,
context.colors.onSurfaceVariant,
),
final s when s.contains('雷') => (Symbols.flash_on_rounded, Colors.amber),
final s when s.contains('雪') => (Symbols.snowflake_rounded, Colors.lightBlue[200]),
_ => (Symbols.wb_cloudy_rounded, context.colors.onSurfaceVariant),
};

@override
Widget build(BuildContext context) {
final time = item['time'] as String? ?? '';
final weather = item['weather'] as String? ?? '';
final temp = (item['temperature'] as num?)?.toDouble() ?? 0.0;
final pop = item['pop'] as int? ?? 0;

final (icon, color) = _weatherIcon(context, weather);

final primaryColor = isNow
? context.colors.onSurface
: context.colors.onSurface.withValues(alpha: 0.82);
final labelColor = isNow
? context.colors.onSurface
: context.colors.onSurfaceVariant.withValues(alpha: 0.75);

return SizedBox(
width: _kColumnWidth,
child: Column(
mainAxisSize: .min,
children: [
Text(
isNow ? '現在'.i18n : time,
style: context.texts.labelSmall?.copyWith(
color: labelColor,
fontWeight: isNow ? .w700 : .w500,
height: 1,
),
),
const SizedBox(height: 10),
Icon(icon, color: color, fill: 1, size: 24),
if (pop > 0) ...[
const SizedBox(height: 6),
Row(
mainAxisSize: .min,
mainAxisAlignment: .center,
children: [
Icon(
Symbols.water_drop_rounded,
size: 10,
color: Colors.blueAccent.withValues(alpha: 0.85),
),
const SizedBox(width: 2),
Text(
'$pop%',
style: TextStyle(
fontSize: 10,
color: Colors.blueAccent.withValues(alpha: 0.85),
fontWeight: .w600,
height: 1,
),
),
],
),
],
const SizedBox(height: 10),
Text(
'${temp.round()}°',
style: context.texts.titleMedium?.copyWith(
fontWeight: isNow ? .w700 : .w600,
color: primaryColor,
height: 1,
),
),
],
),
);
}
}
32 changes: 21 additions & 11 deletions lib/app/new_home/_widgets/radar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Radar extends StatefulWidget {

class _RadarState extends State<Radar> with WidgetsBindingObserver, RouteAware {
MapLibreMapController? _mapController;
bool _homeListenerAdded = false;
HomeModel? _homeModel;

/// Resolves to the list of available radar timestamps once fetched.
late Future<List<String>> _radarListFuture;
Expand Down Expand Up @@ -109,9 +109,11 @@ class _RadarState extends State<Radar> with WidgetsBindingObserver, RouteAware {
final route = ModalRoute.of(context);
if (route != null) routeObserver.subscribe(this, route);

if (!_homeListenerAdded) {
context.home.addListener(_onHomeModelChanged);
_homeListenerAdded = true;
final model = context.home;
if (_homeModel != model) {
_homeModel?.removeListener(_onHomeModelChanged);
_homeModel = model;
model.addListener(_onHomeModelChanged);
}
}

Expand All @@ -121,14 +123,22 @@ class _RadarState extends State<Radar> with WidgetsBindingObserver, RouteAware {
final targetLocation = userLocation ?? DpipMap.kTaiwanCenter;
final targetZoom = userLocation != null ? DpipMap.kUserLocationZoom : DpipMap.kTaiwanZoom;

return Padding(
padding: .symmetric(horizontal: 12, vertical: 4),
child: Card(
clipBehavior: .antiAlias,
return Container(
margin: const .symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: context.colors.surfaceContainerLow,
borderRadius: .circular(16),
border: Border.all(
color: context.colors.outlineVariant.withValues(alpha: 0.5),
),
),
clipBehavior: .antiAlias,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => MapRoute(layers: 'radar').push(context),
onTap: () => const MapRoute(layers: 'radar').push(context),
child: Padding(
padding: .all(12),
padding: const .all(12),
child: Column(
crossAxisAlignment: .start,
spacing: 12,
Expand Down Expand Up @@ -209,7 +219,7 @@ class _RadarState extends State<Radar> with WidgetsBindingObserver, RouteAware {

@override
void dispose() {
context.home.removeListener(_onHomeModelChanged);
_homeModel?.removeListener(_onHomeModelChanged);
routeObserver.unsubscribe(this);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
Expand Down
Loading