Compare commits

...

3 commits

Author SHA1 Message Date
31aea64608
Add about page to Fluent UI 2022-11-19 06:01:58 +01:00
a5615fe3cb
Add Settings page 2022-11-19 05:35:51 +01:00
342b870e93
Add touch scrolling on Linux
Workaround: on Linux, touch is recognized as mouse by Flutter,
so this enables scroll by dragging with mouse.
2022-11-19 05:33:39 +01:00
17 changed files with 688 additions and 49 deletions

View file

@ -1,3 +1,8 @@
v2.7.10
Add about page to Fluent UI.
Add settings page, allowing changing between UIs.
Add touch scrolling on Linux.
v2.7.9
Add Fluent UI for Windows and Linux.
Add split view in landscape when viewing a train.

194
codemagic.yaml Normal file

File diff suppressed because one or more lines are too long

View file

@ -5,39 +5,54 @@ class CupertinoListTile extends StatelessWidget {
final Widget? title;
final Widget? subtitle;
final Widget? trailing;
final void Function()? onTap;
const CupertinoListTile({ Key? key, this.leading, this.title, this.subtitle, this.trailing, }) : super(key: key);
const CupertinoListTile({ super.key, this.leading, this.title, this.subtitle, this.trailing, this.onTap, });
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
children: [
if (leading != null)
leading!,
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (title != null)
title!,
if (subtitle != null)
CupertinoTheme(
data: CupertinoTheme.of(context).copyWith(
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(
fontSize: CupertinoTheme.of(context).textTheme.textStyle.fontSize! - 2,
)
)
),
child: subtitle!,
),
],
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
if (leading != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: leading!,
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null)
title!,
if (subtitle != null)
CupertinoTheme(
data: CupertinoTheme.of(context).copyWith(
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(
fontSize: CupertinoTheme.of(context).textTheme.textStyle.fontSize! - 2,
)
)
),
child: subtitle!,
),
],
),
),
),
),
if (trailing != null)
trailing!,
],
if (trailing != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: trailing!,
),
],
),
);
}
}

View file

@ -1,5 +1,8 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' as f;
import 'package:flutter/cupertino.dart' as c;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' as m;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@ -7,6 +10,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:info_tren/pages/main/main_page.dart';
import 'package:info_tren/pages/settings/setings_page.dart';
import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart';
import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart';
import 'package:info_tren/pages/train_info_page/view_train/train_info.dart';
@ -33,6 +37,9 @@ Map<String, WidgetBuilder> get routes => {
AboutPage.routeName: (context) {
return const AboutPage();
},
SettingsPage.routeName: (context) {
return const SettingsPage();
},
SelectTrainPage.routeName: (context) {
return const SelectTrainPage();
},
@ -59,6 +66,36 @@ Map<String, WidgetBuilder> get routes => {
},
};
class DragFluentScrollBevahior extends f.FluentScrollBehavior {
const DragFluentScrollBevahior();
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
};
}
class DragCupertinoScrollBevahior extends c.CupertinoScrollBehavior {
const DragCupertinoScrollBevahior();
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
};
}
class DragMaterialScrollBevahior extends m.MaterialScrollBehavior {
const DragMaterialScrollBevahior();
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
};
}
class StartPoint extends ConsumerWidget {
final String appTitle = 'Info Tren';
@ -83,6 +120,7 @@ class StartPoint extends ConsumerWidget {
// ),
// ),
),
scrollBehavior: Platform.isLinux ? const DragCupertinoScrollBevahior() : null,
routes: routes,
),
);
@ -95,6 +133,7 @@ class StartPoint extends ConsumerWidget {
accentColor: f.Colors.blue,
),
routes: routes,
scrollBehavior: Platform.isLinux ? const DragFluentScrollBevahior() : const f.FluentScrollBehavior(),
);
}
else {
@ -110,6 +149,7 @@ class StartPoint extends ConsumerWidget {
useMaterial3: true,
// fontFamily: 'Atkinson Hyperlegible',
),
scrollBehavior: Platform.isLinux ? const DragMaterialScrollBevahior() : null,
routes: routes,
);
}

View file

@ -4,6 +4,14 @@ enum UiDesign {
FLUENT,
}
extension UIName on UiDesign {
String get userInterfaceName => (const {
UiDesign.MATERIAL: 'Material',
UiDesign.CUPERTINO: 'Cupertino',
UiDesign.FLUENT: 'Fluent',
})[this]!;
}
class UnmatchedUiDesignException implements Exception {
final UiDesign uiDesign;

View file

@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/api/releases.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/about/about_page_cupertino.dart';
import 'package:info_tren/pages/about/about_page_fluent.dart';
import 'package:info_tren/pages/about/about_page_material.dart';
import 'package:info_tren/providers.dart';
import 'package:package_info_plus/package_info_plus.dart';
@ -21,6 +22,8 @@ class AboutPage extends ConsumerWidget {
return const AboutPageMaterial();
case UiDesign.CUPERTINO:
return const AboutPageCupertino();
case UiDesign.FLUENT:
return const AboutPageFluent();
default:
throw UnmatchedUiDesignException(uiDesign);
}
@ -31,7 +34,7 @@ abstract class AboutPageShared extends StatefulWidget {
const AboutPageShared({super.key});
}
abstract class AboutPageState extends State<AboutPageShared> {
abstract class AboutPageState<T extends AboutPageShared> extends State<T> {
static const String download = String.fromEnvironment('DOWNLOAD');
final String pageTitle = 'Despre aplicație';

View file

@ -3,14 +3,14 @@ import 'package:info_tren/components/cupertino_divider.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPageCupertino extends StatefulWidget {
class AboutPageCupertino extends AboutPageShared {
const AboutPageCupertino({super.key});
@override
State<AboutPageShared> createState() => AboutPageStateCupertino();
}
class AboutPageStateCupertino extends AboutPageState {
class AboutPageStateCupertino extends AboutPageState<AboutPageCupertino> {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(

View file

@ -0,0 +1,140 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPageFluent extends AboutPageShared {
const AboutPageFluent({super.key});
@override
State<AboutPageShared> createState() => AboutPageStateFluent();
}
class AboutPageStateFluent extends AboutPageState<AboutPageFluent> {
@override
Widget build(BuildContext context) {
return NavigationView(
appBar: NavigationAppBar(
title: Text(pageTitle),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
'Info Tren',
style: FluentTheme.of(context).typography.display,
),
),
if (packageInfo != null)
Center(
child: Text(
packageInfo!.packageName,
style: FluentTheme.of(context).typography.caption,
),
),
// ListTile(
// title: Text(versionTitleText),
// subtitle: localChangelog.isEmpty ? null : Text(localChangelog.first.title),
// ),
const Divider(),
for (final log in mergedChangelogs) ...[
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
log.version.toString(),
style: FluentTheme.of(context).typography.title,
),
),
if (localChangelog.isNotEmpty && log.version == localChangelog.first.version)
Container(
decoration: BoxDecoration(
border: Border.all(
color: FluentTheme.of(context).inactiveColor,
width: 1,
),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
currentVersionText,
style: const TextStyle(
inherit: true,
),
),
),
),
if (remoteChangelog.isNotEmpty && log.version == remoteChangelog.first.version && (localChangelog.isEmpty || localChangelog.first.version != log.version))
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.green,
width: 1,
),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
latestVersionText,
style: TextStyle(
inherit: true,
color: Colors.green,
),
),
),
),
if (AboutPageState.download == 'apk' && log.apkLink != null)
GestureDetector(
onSecondaryTap: () {
Clipboard.setData(ClipboardData(text: log.apkLink!.toString()));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Link copied to clipboard'),
// ));
},
onLongPress: () {
Clipboard.setData(ClipboardData(text: log.apkLink!.toString()));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Link copied to clipboard'),
// ));
},
onTap: () {
launchUrl(
log.apkLink!,
mode: LaunchMode.externalApplication,
);
},
behavior: HitTestBehavior.translucent,
child: const Tooltip(
message: 'Download APK',
child: Padding(
padding: EdgeInsets.all(4),
child: Icon(FluentIcons.download),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RichText(
text: TextSpan(
text: log.description,
),
),
),
],
],
),
),
);
}
}

View file

@ -3,14 +3,14 @@ import 'package:flutter/services.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPageMaterial extends StatefulWidget {
class AboutPageMaterial extends AboutPageShared {
const AboutPageMaterial({super.key});
@override
State<AboutPageShared> createState() => AboutPageStateMaterial();
}
class AboutPageStateMaterial extends AboutPageState {
class AboutPageStateMaterial extends AboutPageState<AboutPageMaterial> {
@override
Widget build(BuildContext context) {
return Scaffold(
@ -138,4 +138,4 @@ class AboutPageStateMaterial extends AboutPageState {
),
);
}
}
}

View file

@ -5,6 +5,7 @@ import 'package:info_tren/pages/about/about_page.dart';
import 'package:info_tren/pages/main/main_page_cupertino.dart';
import 'package:info_tren/pages/main/main_page_fluent.dart';
import 'package:info_tren/pages/main/main_page_material.dart';
import 'package:info_tren/pages/settings/setings_page.dart';
import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart';
import 'package:info_tren/pages/train_info_page/select_train/select_train.dart';
import 'package:info_tren/providers.dart';
@ -36,6 +37,12 @@ abstract class MainPageShared extends StatelessWidget {
const MainPageShared({super.key});
List<MainPageAction> get popupMenu => [
MainPageAction(
name: 'Setări',
action: (context) {
Navigator.of(context).pushNamed(SettingsPage.routeName);
},
),
MainPageAction(
name: 'Despre aplicație',
action: (context) {
@ -85,5 +92,9 @@ class MainPageAction {
final String? description;
final void Function(BuildContext context)? action;
MainPageAction({required this.name, this.action, this.description});
MainPageAction({
required this.name,
this.action,
this.description,
});
}

View file

@ -10,6 +10,22 @@ class MainPageFluent extends MainPageShared {
appBar: NavigationAppBar(
automaticallyImplyLeading: false,
title: Text(pageTitle),
actions: Row(
mainAxisSize: MainAxisSize.min,
children: popupMenu.map((i) => Center(
child: SizedBox(
width: 32,
height: 32,
child: IconButton(
icon: Icon({
'Setări': FluentIcons.settings,
'Despre aplicație': FluentIcons.info,
}[i.name]),
onPressed: i.action != null ? () => i.action!(context) : null,
),
),
)).toList(),
),
// centerTitle: true,
// actions: [
// PopupMenuButton<int>(

View file

@ -0,0 +1,37 @@
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/settings/settings_page_cupertino.dart';
import 'package:info_tren/pages/settings/settings_page_fluent.dart';
import 'package:info_tren/pages/settings/settings_page_material.dart';
import 'package:info_tren/providers.dart';
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key,});
static const String routeName = '/settings';
@override
Widget build(BuildContext context, WidgetRef ref) {
final uiDesign = ref.watch(uiDesignProvider);
switch (uiDesign) {
case UiDesign.MATERIAL:
return const SettingsPageMaterial();
case UiDesign.CUPERTINO:
return const SettingsPageCupertino();
case UiDesign.FLUENT:
return const SettingsPageFluent();
default:
throw UnmatchedUiDesignException(uiDesign);
}
}
}
abstract class SettingsPageShared extends StatelessWidget {
final String pageTitle = 'Setări';
final String appearanceTitle = 'Aspect';
const SettingsPageShared({super.key});
}

View file

@ -0,0 +1,77 @@
import 'package:flutter/cupertino.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/components/cupertino_listtile.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/settings/setings_page.dart';
import 'package:info_tren/providers.dart';
class SettingsPageCupertino extends SettingsPageShared {
const SettingsPageCupertino({super.key,});
Future<T?> singleChoice<T>({required BuildContext context, required List<T> choices, required String Function(T) labelBuilder, String? title}) async {
return await showCupertinoModalPopup<T>(
context: context,
builder: (context) {
return CupertinoActionSheet(
title: title != null ? Text(title) : null,
actions: choices.map((c) => CupertinoActionSheetAction(
onPressed: () {
Navigator.of(context).pop(c);
},
child: Text(labelBuilder(c)),
)).toList(),
cancelButton: CupertinoActionSheetAction(
child: const Text('Anulare'),
onPressed: () {
Navigator.of(context).pop();
},
),
);
},
);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
previousPageTitle: 'Info Tren',
middle: Text(pageTitle),
),
child: Builder(
builder: (context) {
final mq = MediaQuery.of(context);
return SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: mq.padding.top,
),
Consumer(
builder: (context, ref, _) {
final currentUiDesign = ref.watch(uiDesignProvider);
return CupertinoListTile(
title: Text(appearanceTitle),
trailing: Text(currentUiDesign.userInterfaceName),
onTap: () async {
final choice = await singleChoice(
context: context,
choices: UiDesign.values,
labelBuilder: (UiDesign ud) => ud.userInterfaceName,
title: appearanceTitle,
);
if (choice != null) {
ref.read(uiDesignProvider.notifier).set(choice);
}
},
);
},
),
],
),
);
},
),
);
}
}

View file

@ -0,0 +1,42 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/settings/setings_page.dart';
import 'package:info_tren/providers.dart';
class SettingsPageFluent extends SettingsPageShared {
const SettingsPageFluent({super.key,});
@override
Widget build(BuildContext context) {
return NavigationView(
appBar: NavigationAppBar(
title: Text(pageTitle),
),
content: SingleChildScrollView(
child: Column(
children: [
Consumer(
builder: (context, ref, _) {
final currentUiDesign = ref.watch(uiDesignProvider);
return ListTile(
title: Text(appearanceTitle),
trailing: ComboBox<UiDesign>(
items: UiDesign.values.map((d) => ComboBoxItem(
value: d,
child: Text(d.userInterfaceName),
)).toList(),
value: currentUiDesign,
onChanged: (newUiDesign) {
ref.read(uiDesignProvider.notifier).set(newUiDesign);
},
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:info_tren/models.dart';
import 'package:info_tren/pages/settings/setings_page.dart';
import 'package:info_tren/providers.dart';
class SettingsPageMaterial extends SettingsPageShared {
const SettingsPageMaterial({super.key,});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(pageTitle),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
children: [
Consumer(
builder: (context, ref, _) {
final currentUiDesign = ref.watch(uiDesignProvider);
return ListTile(
title: Text(appearanceTitle),
trailing: DropdownButton<UiDesign>(
items: UiDesign.values.map((d) => DropdownMenuItem(
value: d,
child: Text(d.userInterfaceName),
)).toList(),
value: currentUiDesign,
onChanged: (newUiDesign) {
ref.read(uiDesignProvider.notifier).set(newUiDesign);
},
),
);
},
),
],
),
),
);
}
}

View file

@ -13,22 +13,30 @@ final sharedPreferencesProvider = Provider<SharedPreferences>(
(_) => throw UnimplementedError('Please override in ProviderScope'),
);
final uiDesignProvider = Provider((ref) {
final sharedPreferences = ref.watch(sharedPreferencesProvider);
final stored = sharedPreferences.getString('uiDesign');
final design = UiDesign.values.where((element) => element.name == stored).firstOrNull ?? defaultUiDesign;
return design;
});
Future<void> setUiDesign(Ref ref, UiDesign? design) async {
final sharedPreferences = ref.watch(sharedPreferencesProvider);
if (design != null) {
await sharedPreferences.setString('uiDesign', design.name);
class UiDesignNotifier extends StateNotifier<UiDesign> {
final SharedPreferences sharedPreferences;
UiDesignNotifier({required this.sharedPreferences,}) : super(UiDesign.MATERIAL) {
final stored = sharedPreferences.getString('uiDesign');
final design = UiDesign.values.where((element) => element.name == stored).firstOrNull ?? defaultUiDesign;
state = design;
}
else {
await sharedPreferences.remove('uiDesign');
void set(UiDesign? design) async {
if (design != null) {
await sharedPreferences.setString('uiDesign', design.name);
}
else {
await sharedPreferences.remove('uiDesign');
}
state = design ?? defaultUiDesign;
}
ref.invalidate(uiDesignProvider);
}
final uiDesignProvider = StateNotifierProvider<UiDesignNotifier, UiDesign>(
(ref) => UiDesignNotifier(
sharedPreferences: ref.watch(sharedPreferencesProvider),
),
);
final trainInfoArgumentsProvider = Provider<TrainInfoArguments>(
(_) => throw UnimplementedError('Please override in ProviderScope'),

View file

@ -11,7 +11,7 @@ description: O aplicație de vizualizare a datelor puse la dispoziție de Inform
# 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: 2.7.9
version: 2.7.10
environment:
sdk: ">=2.17.0 <3.0.0"