Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sidebar layout part 1 #6

Merged
merged 4 commits into from
Apr 21, 2024
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ app.*.map.json
/android/app/release

.vscode/settings.json
assets/apikey.json
3 changes: 3 additions & 0 deletions assets/apikey.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"apiKey": ""
}
7 changes: 7 additions & 0 deletions lib/build_context_x.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:flutter/material.dart';

extension BuildContextX on BuildContext {
ThemeData get theme => Theme.of(this);
bool get light => theme.brightness == Brightness.light;
MediaQueryData get mq => MediaQuery.of(this);
}
7 changes: 7 additions & 0 deletions lib/constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const kAppName = 'pulse';
const kAppTitle = 'Pulse';

const kAppStateFileName = 'appstate.json';
const kSettingsFileName = 'settings.json';
const kFavLocationsFileName = 'favlocations.json';
const kLastLocation = 'lastLocation';
25 changes: 20 additions & 5 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,38 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:geocoding_resolver/geocoding_resolver.dart';
import 'package:geolocator/geolocator.dart';
import 'package:open_weather_client/open_weather.dart';
import 'package:watch_it/watch_it.dart';
import 'package:yaru/yaru.dart';

import 'src/app/app.dart';
import 'src/locations/locations_service.dart';
import 'src/weather/weather_model.dart';

Future<void> main() async {
await YaruWindowTitleBar.ensureInitialized();
final apiKey = await loadApiKey();
if (apiKey != null && apiKey.isNotEmpty) {
di.registerSingleton(OpenWeather(apiKey: apiKey));
di.registerSingleton(GeoCoder());
final weatherModel =
WeatherModel(openWeather: di<OpenWeather>(), geoCoder: di<GeoCoder>());
di.registerSingleton<OpenWeather>(OpenWeather(apiKey: apiKey));
di.registerSingleton<GeoCoder>(GeoCoder());
di.registerSingleton<GeolocatorPlatform>(GeolocatorPlatform.instance);
di.registerSingleton<LocationsService>(
LocationsService(),
dispose: (s) => s.dispose(),
);
final weatherModel = WeatherModel(
locationsService: di<LocationsService>(),
openWeather: di<OpenWeather>(),
geoCoder: di<GeoCoder>(),
geolocatorPlatform: di<GeolocatorPlatform>(),
);
await weatherModel.init();
di.registerSingleton(weatherModel);

di.registerSingleton(
weatherModel,
dispose: (s) => s.dispose(),
);

runApp(const App());
} else {
Expand Down
72 changes: 71 additions & 1 deletion lib/src/app/app.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:watch_it/watch_it.dart';
import 'package:yaru/yaru.dart';

import '../../weather.dart';
import '../weather/view/city_search_field.dart';
import '../weather/weather_model.dart';

class App extends StatelessWidget {
const App({super.key});
Expand All @@ -15,7 +18,7 @@ class App extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: yaruLight,
darkTheme: yaruDark,
home: const WeatherPage(),
home: const MasterDetailPage(),
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
Expand All @@ -28,3 +31,70 @@ class App extends StatelessWidget {
);
}
}

class MasterDetailPage extends StatelessWidget with WatchItMixin {
const MasterDetailPage({super.key});

@override
Widget build(BuildContext context) {
final model = di<WeatherModel>();
final favLocationsLength =
watchPropertyValue((WeatherModel m) => m.favLocations.length);
final favLocations = watchPropertyValue((WeatherModel m) => m.favLocations);
final lastLocation = watchPropertyValue((WeatherModel m) => m.lastLocation);
return YaruMasterDetailPage(
controller: YaruPageController(
length: favLocationsLength == 0 ? 1 : favLocationsLength,
),
tileBuilder: (context, index, selected, availableWidth) {
final location = favLocations.elementAt(index);
return YaruMasterTile(
// TODO: assign pages to location
onTap: () => model.init(cityName: location),
selected: lastLocation == location,
title: Text(
favLocations.elementAt(index),
),
trailing: favLocationsLength > 1
? IconButton(
padding: EdgeInsets.zero,
onPressed: () {
model.removeFavLocation(location).then(
(value) => model.init(
cityName: favLocations.lastOrNull,
),
);
},
icon: const Icon(
YaruIcons.window_close,
),
)
: null,
);
},
pageBuilder: (context, index) {
return const WeatherPage();
},
appBar: YaruDialogTitleBar(
backgroundColor: YaruMasterDetailTheme.of(context).sideBarColor,
border: BorderSide.none,
style: YaruTitleBarStyle.undecorated,
leading: Center(
child: YaruIconButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.location_on,
size: 16,
),
onPressed: () => model.init(cityName: null),
),
),
titleSpacing: 0,
title: const Padding(
padding: EdgeInsets.only(right: 15),
child: CitySearchField(),
),
),
);
}
}
55 changes: 55 additions & 0 deletions lib/src/locations/locations_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'dart:async';

import '../../constants.dart';
import '../../utils.dart';

class LocationsService {
String? _lastLocation;
String? get lastLocation => _lastLocation;
void setLastLocation(String? value) {
if (value == _lastLocation) return;
writeAppState(kLastLocation, value).then((_) {
_lastLocationController.add(true);
_lastLocation = value;
});
}

final _lastLocationController = StreamController<bool>.broadcast();
Stream<bool> get lastLocationChanged => _lastLocationController.stream;

Set<String> _favLocations = {};
Set<String> get favLocations => _favLocations;
bool isFavLocation(String value) => _favLocations.contains(value);
final _favLocationsController = StreamController<bool>.broadcast();
Stream<bool> get favLocationsChanged => _favLocationsController.stream;

void addFavLocation(String name) {
if (favLocations.contains(name)) return;
_favLocations.add(name);
writeStringIterable(
iterable: _favLocations,
filename: kFavLocationsFileName,
).then((_) => _favLocationsController.add(true));
}

Future<void> removeFavLocation(String name) async {
if (!favLocations.contains(name)) return;
_favLocations.remove(name);
return writeStringIterable(
iterable: _favLocations,
filename: kFavLocationsFileName,
).then((_) => _favLocationsController.add(true));
}

Future<void> init() async {
_lastLocation = (await readAppState(kLastLocation)) as String?;
_favLocations = Set.from(
(await readStringIterable(filename: kFavLocationsFileName) ?? <String>{}),
);
}

Future<void> dispose() async {
await _favLocationsController.close();
await _lastLocationController.close();
}
}
18 changes: 4 additions & 14 deletions lib/src/weather/view/city_search_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,15 @@ class _CitySearchFieldState extends State<CitySearchField> {
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.w500),
decoration: InputDecoration(
prefixIcon: const Icon(
decoration: const InputDecoration(
prefixIcon: Icon(
YaruIcons.search,
size: 15,
),
prefixIconConstraints:
const BoxConstraints(minWidth: 35, minHeight: 30),
contentPadding: const EdgeInsets.all(8),
prefixIconConstraints: BoxConstraints(minWidth: 35, minHeight: 30),
contentPadding: EdgeInsets.all(8),
filled: true,
hintText: 'Cityname',
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.circular(40),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.circular(40),
),
fillColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05),
),
);
return textField;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/weather/view/forecast_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_weather_bg_null_safety/bg/weather_bg.dart';
import 'package:flutter_weather_bg_null_safety/flutter_weather_bg.dart';
import 'package:open_weather_client/models/weather_data.dart';
import '../utils.dart';
import '../weather_utils.dart';
import '../../../string_x.dart';
import '../weather_data_x.dart';

Expand Down
53 changes: 17 additions & 36 deletions lib/src/weather/view/today_tile.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_weather_bg_null_safety/bg/weather_bg.dart';
import 'package:flutter_weather_bg_null_safety/flutter_weather_bg.dart';
import 'package:open_weather_client/models/weather_data.dart';
import '../utils.dart';
import '../../../build_context_x.dart';
import '../weather_utils.dart';
import '../../../string_x.dart';
import '../weather_data_x.dart';

Expand All @@ -16,6 +15,7 @@ class TodayTile extends StatelessWidget {
final String? day;
final EdgeInsets padding;
final String? time;
final BorderRadiusGeometry? borderRadius;

const TodayTile({
super.key,
Expand All @@ -28,18 +28,18 @@ class TodayTile extends StatelessWidget {
this.day,
required this.padding,
this.time,
this.borderRadius,
});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final light = theme.brightness == Brightness.light;
final theme = context.theme;
final style = theme.textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontSize: 20,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.8),
color: Colors.black.withOpacity(0.9),
offset: const Offset(0, 1),
blurRadius: 3,
),
Expand Down Expand Up @@ -110,41 +110,22 @@ class TodayTile extends StatelessWidget {
),
];

final banner = Card(
child: Stack(
children: [
Opacity(
opacity: light ? 1 : 0.4,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: WeatherBg(
weatherType: getWeatherType(data),
width: width ?? double.infinity,
height: height ?? double.infinity,
),
),
),
Center(
child: Wrap(
direction: Axis.vertical,
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 20,
runSpacing: 20,
runAlignment: WrapAlignment.center,
children: children,
),
),
],
),
);

return SizedBox(
width: width,
height: height,
child: Padding(
padding: padding,
child: banner,
child: Center(
child: Wrap(
direction: Axis.vertical,
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 20,
runSpacing: 20,
runAlignment: WrapAlignment.center,
children: children,
),
),
),
);
}
Expand Down
Loading