Function client to send GET /path
to server (#4)
* Created default project * Added client to .vscode/launch.json * Ignore .vscode/settings.json * Some data scraping * Client skeleton * Client data types and fromJson() functions * Final touches on JSON * Added ApiService * ApiService changes * Prototype for stops * ViewModel and UI * Basic client UI
This commit is contained in:
parent
f18509ff1d
commit
dc9a3b0e1d
56 changed files with 4919 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.vscode/settings.json
|
14
.vscode/cspell.json
vendored
Normal file
14
.vscode/cspell.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"version": "0.2",
|
||||||
|
"ignorePaths": [
|
||||||
|
"src/client/data/**"
|
||||||
|
],
|
||||||
|
"dictionaryDefinitions": [],
|
||||||
|
"dictionaries": [],
|
||||||
|
"words": [
|
||||||
|
"dotw",
|
||||||
|
"Occt"
|
||||||
|
],
|
||||||
|
"ignoreWords": [],
|
||||||
|
"import": []
|
||||||
|
}
|
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "client (windows)",
|
||||||
|
"cwd": "src\\client",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"args": ["-d", "windows"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "client (web)",
|
||||||
|
"cwd": "src\\client",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"args": ["-d", "chrome"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
45
src/client/.gitignore
vendored
Normal file
45
src/client/.gitignore
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
33
src/client/.metadata
Normal file
33
src/client/.metadata
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "3e493a3e4d0a5c99fa7da51faae354e95a9a1abe"
|
||||||
|
channel: "beta"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
|
||||||
|
base_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
|
||||||
|
- platform: web
|
||||||
|
create_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
|
||||||
|
base_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
|
||||||
|
base_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
16
src/client/README.md
Normal file
16
src/client/README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# client
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
52
src/client/analysis_options.yaml
Normal file
52
src/client/analysis_options.yaml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints. See the following for docs:
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
include: package:very_good_analysis/analysis_options.yaml # has more lints
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
language:
|
||||||
|
# Strict casts isn't helpful with null safety. It only notifies you on `dynamic`,
|
||||||
|
# which happens all the time in JSON.
|
||||||
|
#
|
||||||
|
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-casts.md
|
||||||
|
strict-casts: false
|
||||||
|
|
||||||
|
# Don't let any types be inferred as `dynamic`.
|
||||||
|
#
|
||||||
|
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-inference.md
|
||||||
|
strict-inference: true
|
||||||
|
|
||||||
|
# Don't let Dart infer the wrong type on the left side of an assignment.
|
||||||
|
#
|
||||||
|
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-raw-types.md
|
||||||
|
strict-raw-types: true
|
||||||
|
|
||||||
|
exclude:
|
||||||
|
- lib/generated/**.dart
|
||||||
|
- test/**.dart
|
||||||
|
- example/**.dart
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
# Rules NOT in package:very_good_analysis
|
||||||
|
prefer_double_quotes: true
|
||||||
|
prefer_expression_function_bodies: true
|
||||||
|
|
||||||
|
# Rules to be disabled from package:very_good_analysis
|
||||||
|
prefer_single_quotes: false # prefer_double_quotes
|
||||||
|
lines_longer_than_80_chars: false # lines should be at most 100 chars
|
||||||
|
sort_pub_dependencies: false # Sort dependencies by function
|
||||||
|
use_key_in_widget_constructors: false # not in Flutter apps
|
||||||
|
directives_ordering: false # sort dart, then flutter, then package imports
|
||||||
|
always_use_package_imports: false # not when importing sibling files
|
||||||
|
sort_constructors_first: false # final properties, then constructor
|
||||||
|
avoid_dynamic_calls: false # this lint takes over errors in the IDE
|
||||||
|
one_member_abstracts: false # abstract classes are good for interfaces
|
||||||
|
cascade_invocations: false # cascades are often harder to read
|
||||||
|
|
||||||
|
# Temporarily disabled until we are ready to document
|
||||||
|
public_member_api_docs: false
|
132
src/client/bin/occt.dart
Normal file
132
src/client/bin/occt.dart
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
// Entrypoints can print
|
||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import "dart:convert";
|
||||||
|
import "dart:io";
|
||||||
|
import "package:http/http.dart";
|
||||||
|
import "package:html/parser.dart";
|
||||||
|
|
||||||
|
typedef Json = Map<String, dynamic>;
|
||||||
|
|
||||||
|
extension <E> on List<E> {
|
||||||
|
Iterable<(int, E)> enumerate() sync* {
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
yield (i, this[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final spotClient = Client();
|
||||||
|
|
||||||
|
class OcctStop {
|
||||||
|
static final Map<int, OcctStop> stopsById = {};
|
||||||
|
static final Map<String, OcctStop> stopsByName = {};
|
||||||
|
|
||||||
|
static Future<void> readStops() async {
|
||||||
|
final file = File("data/stops.json");
|
||||||
|
final json = jsonDecode(await file.readAsString()) as List;
|
||||||
|
for (final stopJson in json.cast<Json>()) {
|
||||||
|
final name = stopJson["name"] as String;
|
||||||
|
final id = stopJson["id"] as int;
|
||||||
|
final routeId = stopJson["rid"] as int;
|
||||||
|
var stop = stopsById[id];
|
||||||
|
if (stop == null) {
|
||||||
|
stop = OcctStop(id, name);
|
||||||
|
stopsById[id] = stop;
|
||||||
|
stopsByName[name] = stop;
|
||||||
|
}
|
||||||
|
stop.routes.add(routeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final int id;
|
||||||
|
final List<int> routes = [];
|
||||||
|
OcctStop(this.id, this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class OcctRoute {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String abbr;
|
||||||
|
final List<OcctRouteStop> stops = [];
|
||||||
|
final String encodedLine;
|
||||||
|
final String hexColor;
|
||||||
|
|
||||||
|
OcctRoute.fromJson(Json json) :
|
||||||
|
id = json["id"],
|
||||||
|
name = json["name"],
|
||||||
|
abbr = json["abbr"],
|
||||||
|
encodedLine = json["encLine"],
|
||||||
|
hexColor = json["color"];
|
||||||
|
}
|
||||||
|
|
||||||
|
class OcctRouteStop {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final List<String> times = [];
|
||||||
|
|
||||||
|
OcctRouteStop(this.id, this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"route_id": "int",
|
||||||
|
"stops": [
|
||||||
|
{
|
||||||
|
"stop_id": "int",
|
||||||
|
"stop_name": "string",
|
||||||
|
"times": [ "HH:MM PM" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
String getStopName(String text) {
|
||||||
|
final regex = RegExp("(?:Returns to|Arrives at|Leaves)? ?(?:the )?(.+)");
|
||||||
|
const stopNameSubstitutions = {
|
||||||
|
"Binghamton University": "University Union",
|
||||||
|
"Innovative Tech Complex": "Innovative Technology Center",
|
||||||
|
"UClub / University Plaza": "UCLUB",
|
||||||
|
};
|
||||||
|
print("Trying to find stop name in [$text]");
|
||||||
|
final match = regex.firstMatch(text)!;
|
||||||
|
final name = match.group(1)!;
|
||||||
|
return stopNameSubstitutions[name] ?? name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
await OcctStop.readStops();
|
||||||
|
final uri = Uri.parse("https://occtransport.org/pages/routes/iu.html");
|
||||||
|
final response = await get(uri);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
print("Got non-200 status: ");
|
||||||
|
print(response.body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final html = parse(response.body);
|
||||||
|
final table = html.getElementsByTagName("tbody").first;
|
||||||
|
print(table.children.first.innerHtml);
|
||||||
|
final numStops = table.children.first.children.length;
|
||||||
|
print("There are $numStops stops");
|
||||||
|
|
||||||
|
final stopsHtml = html.getElementsByClassName("sch-notes").first;
|
||||||
|
final routeStops = <OcctRouteStop>[];
|
||||||
|
for (final stopHtml in stopsHtml.children) {
|
||||||
|
final sourceName = stopHtml.text.substring(2); // eg, 1 Leaves Mohawk
|
||||||
|
final name = getStopName(sourceName);
|
||||||
|
final id = OcctStop.stopsByName[name]?.id;
|
||||||
|
if (id == null) {
|
||||||
|
print("Could not find a stop with the name: $name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
routeStops.add(OcctRouteStop(id, name));
|
||||||
|
}
|
||||||
|
for (final row in table.children) {
|
||||||
|
for (final (index, timeHtml) in row.children.enumerate()) {
|
||||||
|
final time = timeHtml.innerHtml;
|
||||||
|
routeStops[index].times.add(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(routeStops);
|
||||||
|
}
|
442
src/client/data/routes.json
Normal file
442
src/client/data/routes.json
Normal file
|
@ -0,0 +1,442 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Westside Outbound",
|
||||||
|
"abbr": "WS O",
|
||||||
|
"stops": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
132,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
18,
|
||||||
|
19,
|
||||||
|
20,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "wb{_GhitnMBAFh@LZ@nCO~CIfA??IpAUjBM|@AVSbA_@`B??e@|Ao@rA}@lAm@f@??o@PSCy@?aAAy@O??eAYi@W??WWkA|Be@v@??eBjBy@l@s@b@??YNuBv@gB`@??aCd@SRUb@iAO??s@?}DNBwA??D{CTcAPk@V_@??`CeDRi@Fc@?YA_@EUM]W_@OKe@M??cIS??sHU??mCOsC]??mD}@iA_@??yCkAyB_A??iH{C??mGoCUY??oAm@g@[g@k@u@iA??e@q@Q]GYCWAe@B]NWBg@CUWMMaA??i@iGI]E{@??m@wK??s@kL??u@kL??u@mL??QoCo@qG??]gD[_E??s@qJ??YkDUqA??Qw@cB{F??w@eB]a@kAmA??_CiB~@iC??rA_EdAgC??`E}I??`EcJ??`DgH^u@??fAwBdCuF??nD_I??rCmG??nBkE??nBiE??pBmE??pBkE??rBkE??xByE??nAuC`@sA??tAqG??^aBTuAXyB??dAeJ??`A_J??n@cGDwB??FwK??@aAlCf@PFTA??XGPIV[Re@F[L{A~B^????|Dv@",
|
||||||
|
"color": "#191970",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 1,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Westside Inbound",
|
||||||
|
"abbr": "WS I",
|
||||||
|
"stops": [
|
||||||
|
42,
|
||||||
|
43,
|
||||||
|
44,
|
||||||
|
45,
|
||||||
|
46,
|
||||||
|
47,
|
||||||
|
48,
|
||||||
|
49,
|
||||||
|
50,
|
||||||
|
51,
|
||||||
|
52,
|
||||||
|
53,
|
||||||
|
54,
|
||||||
|
55,
|
||||||
|
56,
|
||||||
|
57,
|
||||||
|
58,
|
||||||
|
59,
|
||||||
|
60,
|
||||||
|
32,
|
||||||
|
118,
|
||||||
|
1,
|
||||||
|
61
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "qv|_G`_jnMA?iDq@_@S}B_@??y@OSFM`BSn@Y^OJ[J??]LmCg@AxA??IrMCb@]xC??UdCiBhP??a@nDg@~C_BjH??k@~Bs@fB{@jB??wE`K??uE~J??uEbK??uEdK??uEbK??w@fBgBlD{@nB??wEfK??wEhK??gB|D_C|G????i@zA??A@zBdBh@h@bAhAVf@??f@lAzAjF\\zA??RlBfAxN@?TbDnA`M??pA|S??pA~S??~@bP?r@??DxAR|C@|@OHCHCLA\\D\\PJFR??Fp@HzAD`@Lp@Zn@X`@XRl@T~Ab@??j@RRBhGhC??pInD??vFfC|Aj@??d@PfBVfBRrBN??~CLzA?`BH@?|IV??d@BTJPNLRL`@Db@?RGf@Sd@ST??QHa@FYCWOSWOc@CU?a@???]EUNcJ??J{K??BcAPo@DIRMXGjAH??dAH~ATpBd@??p@R|@f@jBxA??NTxAjEFh@??^|Cf@|Bb@`B??BXHTp@xAv@|@~@l@??r@Xh@JfAF|@A??XANERKn@Q^WRU??v@gAXg@^_A^uA??h@_CDY@WTaB??LgARyCJaCB]ZOHIBa@?{@???qAGeB????g@DC@Ij@Cd@",
|
||||||
|
"color": "#191970",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 2,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Downtown Center Leroy Outbound",
|
||||||
|
"abbr": "DCL O",
|
||||||
|
"stops": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
33,
|
||||||
|
34,
|
||||||
|
35,
|
||||||
|
36,
|
||||||
|
37,
|
||||||
|
38,
|
||||||
|
39,
|
||||||
|
40,
|
||||||
|
41,
|
||||||
|
42
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "sb{_GlitnM??Fb@LZ@nCO~CMhB??[zCM|@AVq@|C??]pA_@~@Yf@_ApA??k@b@o@PSCy@?aAAo@K??oA]i@Y??WUkA|Bg@z@??cBfBy@l@mAr@a@P??sAd@mAXuCj@??YTUb@o@Ka@Ek@@{BH??aADHsFTcAJ[??DOT]bCgDL[FUDk@??Ai@Kc@O[OSOKSIQC{BE??gJY??eFQ{@G}AS??q@ImAYcBc@{Ag@??gCcA{DaB??{IwD??yCqASWqAo@q@e@??i@q@oAkBWk@EYA[Bq@NWXKr@Mb@OTM??ROd@e@nBsCL]h@q@??lFqG??vCsDlAeB??Zo@hCsDr@kA??vEwH??l@eARe@H[L}@ZiD??z@sL??~@eM??LmAVcBtAgG??d@mBVw@pCmG??fFmL??r@gBTo@tAkG?AlAoFfAkE??dD}K@?pBuIVkA??fAqFd@oD??^kDsA?kBL??{EX??iDRKcE??SuJ??SuJ??SwJ??SmK???cA^{I??`@eL????L{C~A`@??lJ|B??nAZc@uI??e@qJ??k@qJ??e@yH??E]@YM@u@IiFcA??GAIIi@K",
|
||||||
|
"color": "#D2042D",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 3,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Downtown Center Leroy Inbound",
|
||||||
|
"abbr": "DCL I",
|
||||||
|
"stops": [
|
||||||
|
21,
|
||||||
|
22,
|
||||||
|
23,
|
||||||
|
24,
|
||||||
|
25,
|
||||||
|
26,
|
||||||
|
27,
|
||||||
|
28,
|
||||||
|
29,
|
||||||
|
30,
|
||||||
|
31,
|
||||||
|
32,
|
||||||
|
118,
|
||||||
|
61,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "iy|_Gz~inM?A??xB`@H@JClCf@??bB\\t@HJVTpC??`AxP??RrDOFIBS?{EkA??gCo@QxA??_AtIG|@??i@pN??a@bK?z@??TxL??VhM??TnK??PfI??D`B|CQ??~DU??rCSj@???f@?c@`E]hC??Y|AsAlG??qArFk@zB??qCdJ??gAjEcApE??_BjHUn@a@bA??{D~I??_E|I??g@xAoBnI??YxAS|Am@hI??eAzN??g@|FGd@Kh@KXq@pA??uErH??}@xAqBtC??W\\a@x@_CzC??oEtF??gD`Ec@`@??yBfC]Z]R??c@RQ@UCYI??[QY?WLGV?j@BNPJDNJ`A??FnAL`AXt@`@j@??XTXJbD`A??NBjEhB??rElB??~EtB??dF|B??pBr@|B\\@?nALjCRX@??nFJ??tGT??`BBhAFJDZTLRL`@D^??Cj@Kf@U^[Pa@FYCa@YIMKY??G_@?_AEUNaJ??L{M??Ru@DIRMXG|CT??rAR|@RdBd@b@T??p@`@rAhANTf@tA??`@rAN`@V`CL|@??h@dCb@`BBXb@hA??Vd@dAhAj@^n@V`@J??xAJl@?\\A??ZGRKn@Q^Wn@u@??Zg@n@sATq@f@oB??ZaB@WL}@TkBNwB??RaETK??NMBa@?eD??IoA??@Bk@DIj@CZ",
|
||||||
|
"color": "#D2042D",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 4,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"name": "Campus Shuttle",
|
||||||
|
"abbr": "CS",
|
||||||
|
"stops": [
|
||||||
|
61,
|
||||||
|
117,
|
||||||
|
62,
|
||||||
|
63,
|
||||||
|
64,
|
||||||
|
65,
|
||||||
|
110,
|
||||||
|
111,
|
||||||
|
112,
|
||||||
|
113,
|
||||||
|
114,
|
||||||
|
115,
|
||||||
|
116,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "{a{_GdptnM?u@??A_A??AgA??CiA?AAe@Ee@??GkA??KgA??MaAQ{@??g@kBy@uB??aAqBm@y@??MKg@Ye@IS?A?UBe@Pg@ZGL??[f@Sj@M`A??IhB?jB??LtD??@LgAl@WNY\\??_@j@s@vA??e@v@{@x@??GFI@g@Tg@L??a@JWLWDMLEJEZ??B^LP`AtC??Vt@????V`CMj@??I`C??IdCEn@??WxB?jA??J`Av@tB??FRbBgB??f@{@t@{A?ADEx@j@n@V??^JtAJ??p@???RpHA?nAE??nCO??dCU??pAGr@M??bC]??bC]??bC]H?AhASjB??WfAYv@o@pA??s@~@cAdA??cBdB??q@r@]j@??q@CG@]zB?h@`@?@I??TqAV{@??b@w@`AcA??|B_C??|@eAj@cA??Rc@Xw@XoA?AJaAFw@AqAEg@??Gm@Mq@Qk@g@oAKShB{C??|BgE??v@eBp@mB??HW}@o@?A?@YWCAe@zAABG^BfB??Dl@DL??HN}@`B??wAhC??sAxBg@n@??}@|@{@n@??gCnA??u@\\[Jo@L??m@D_AE??a@Gk@SQOO[??e@cBl@g@j@s@??j@aATk@f@aB??j@gCDY@WNeA??RcBTeD??LiC",
|
||||||
|
"color": "#228B22",
|
||||||
|
"type": "Linear",
|
||||||
|
"order": 5,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 0,
|
||||||
|
"useCustomShape": true,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"name": "ITC - UCLUB",
|
||||||
|
"abbr": "IU",
|
||||||
|
"stops": [
|
||||||
|
61,
|
||||||
|
63,
|
||||||
|
64,
|
||||||
|
65,
|
||||||
|
109,
|
||||||
|
66,
|
||||||
|
67,
|
||||||
|
68,
|
||||||
|
134,
|
||||||
|
69
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "ub{_GfbtnMYkBQaAe@gBe@qA??a@aAs@sAs@_Ag@[??]KOAU?UBe@Pg@Zc@t@Qd@??OdAIjB?lBJrC??Bl@gAl@c@\\m@z@e@|@??m@jAWXq@l@I@g@Te@L??c@JWLWDy@OkA]??uAe@uBi@A?{B_@wBK??OAe@_@M[Kq@J}F??P{L??FqE|CM??hBKh@Gb@Q^U??`A{@??e@qAUa@o@m@??QWEQ?a@n@yARoA??\\cCJBD_@??f@uD??BMa@D??[Be@C?B???Cc@ESBMH??ITSpA??CV@VFLLH??bAZ\\HRAGb@In@?AWfBK^??k@pA?N???PDPHNRR??b@`@Zl@??^dAu@n@??k@`@c@PYD??wBL??sAF??kADBqB??HqF??F}ANkC??PiBTaB??t@qE??v@mE??x@oE??x@mE??t@cE??t@eE??`@aCn@R??jA\\T@??`AZ??O~@??e@nC??g@xC??e@lC??QhATJ?AbC|@??lBr@??fA`@d@H??lC`@??lC\\`AX??zCjAXXBN??RbAFbAEx@o@bI??CVxE[??tDWdAOj@O??d@KzCcAn@O??h@Gv@@zBH??lBDr@AF|A??NfDmCR??[JKJUd@G\\?nADjCAt@??IjB]zBa@tAi@nA??e@x@]b@wAtAs@h@??y@f@y@\\o@R_@H??WBy@BaAQIB[T??UPPxA@H?DHpAH|@??????DdB??@bB??@rA@FXMHIBa@?}@???oAGcB??c@B??G@I}@Cw@??AW",
|
||||||
|
"color": "#CF9FFF",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 6,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "UCLUB",
|
||||||
|
"abbr": "UC",
|
||||||
|
"stops": [
|
||||||
|
61,
|
||||||
|
63,
|
||||||
|
64,
|
||||||
|
65,
|
||||||
|
66,
|
||||||
|
67,
|
||||||
|
68,
|
||||||
|
69,
|
||||||
|
134,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "yb{_GxatnMS_BWkA??a@{Ae@oA??a@cAcAgB??c@k@GEg@Yi@I??a@@UFUJg@Z??c@t@Ut@MdAGrB???tAJlC??Br@{@b@c@XUVY`@??}AxC?AWZq@l@I@]N??}Ad@??MFWDg@Iy@S??mAc@??kBi@??cAWu@K??y@MiAG??}@E_@YIKGO??Mw@FaD??HyF??HwF??HyF??HwF??DoCJkB??NsBViC??T_Bl@gD??fAcG??bAqF??bAsF??`AsF?@b@kCn@R??jA\\N?~@X??FB_@|BC???B?m@jD??m@nD??YfBTH??lE`B??jBp@rAT??hDd@??bALXFpAd@??pBv@XXJ`@A?Lp@FbAQ`C??a@nF??Eb@~DY??rEY??`AOpA[??tCaA??~@S^Ev@@h@B??~DJr@AHlB??LvC}BNWDSHKJ??Ud@G\\?nADjCAjAItA[pB??c@~Ae@hAi@~@gAnAm@h@w@l@??qAr@mA`@g@Ji@D_@?a@G??[GI?u@f@RzAH`BH|@M`A",
|
||||||
|
"color": "#BF40BF",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 7,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"name": "University Downtown Center Outbound",
|
||||||
|
"abbr": "UDC O",
|
||||||
|
"stops": [
|
||||||
|
1,
|
||||||
|
21,
|
||||||
|
42
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "sb{_GnitnMF`@LZ@nCO~CSxCUjBM|@AVIb@??y@lDUp@o@rA}@lAWVa@Tc@J??SCy@?aAAc@G{Aa@g@WYWi@m@e@o@e@aAQMg@mB??c@qBc@mDCY@e@G_BS{AD]G[??MSOIWDy@Ok@OuBs@{Aa@kAU??kAQgCMe@_@M[Kq@HcE??ZgU??NiKPaD`@_E??TgBdCeN??zCoPtAeI??xBkMn@oEX{A??dAsFh@cCd@kBzAcF??l@aBdEuK??bGsOn@yB??fAkEl@mDT}ANuA\\iE??|A{Z??d@iJ@oAC}AC_AU_De@wC??o@eCkHaT??w@{Bq@yBk@aCc@qC]qCk@{D??mDuV??e@wCWoBSo@Wa@GG_@Qa@EY@kFp@??qDb@cCVI@eAISIQO[e@Oq@??Cm@Hw@Vs@d@a@F@b@E`@D\\P^l@??FPHf@t@pL??RfEG~@ERQb@y@d@w@O@?cCc@IIs@M",
|
||||||
|
"color": "#FF7F50",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 8,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"name": "University Downtown Center Inbound",
|
||||||
|
"abbr": "UDC I",
|
||||||
|
"stops": [
|
||||||
|
21,
|
||||||
|
32,
|
||||||
|
118,
|
||||||
|
1,
|
||||||
|
61
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "qx|_G~~inMD@??zAX??AyE??E}D@u@???AfAe@n@Ov@MN@~AS??xGw@??~Ca@\\BVJPNLN??N^Jl@bAlH??xA`K??hB`M@?x@bGh@tCNl@??XdA`DfJ??dEzL??l@jB^`Bb@bDRxC??B|BCvA_@hI??{@nP??c@vIa@~E??_@vCe@nCa@fBy@|C??w@dCgEzK??kHdRoAhE??q@hC{@~DcAtFcA|G??UzAsBrK_B~I??eF~Y??a@dCQvA]|DMpCSlI??Y|X??G|El@AlBLpAJl@H??nB^dBd@|@f@`At@??h@b@NTxAjEV`CRrA??b@nBb@`BFb@^`AVb@v@|@`An@??f@R^JzAJl@?h@CNERKb@KJE`@Y??hA{AXg@^_Ah@sBXoAJk@@WD[??\\mCRyCNyC?EZOHIBa@AgA??@eAG_B?Ck@D??Ij@Cd@",
|
||||||
|
"color": "#FF7F50",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 9,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"name": "Main Street Outbound",
|
||||||
|
"abbr": "MS O",
|
||||||
|
"stops": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
70,
|
||||||
|
71,
|
||||||
|
72,
|
||||||
|
73,
|
||||||
|
74,
|
||||||
|
75,
|
||||||
|
76,
|
||||||
|
77,
|
||||||
|
155,
|
||||||
|
78,
|
||||||
|
12,
|
||||||
|
132,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
18,
|
||||||
|
19,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "sb{_GfitnMFh@LZ@nCO~C[`E??[`CAVEX}@vDk@|Ak@`A??y@bA_@VUHYFSCy@?}@A??cAQs@Qa@Q[S??OOkA|Bg@z@WV??kAnAy@l@mAr@cA^??q@VmAXuCj@YTMV??GJo@Ka@Ek@@sBH??iADHsFRaA??\\}@~AwB??n@}@Ri@Fc@?YCg@??I[O[OS[QYGoBE??mGQ??uHW??qAIoC]_B_@??qA]aA[aE_B??eJ{D??kH_DSWo@YAJeAg@q@e@{@w@gAkAeA_A??m@]o@YeAW{@IeA?_@D??{@Ny@Vm@VsDjB??mBbAkB|@o@RyAX??yADqD?KK[GUKSQ??SYKUMk@UuBK]Wg@[Ya@S[Ei@F??}ARIi@Ay@EmH??GaP??EgFBY?e@OmJ??KaFtGYlHa@OmCIaCIsD?kBCkCcEsCr@oCNSb@yA??|@eDd@{AiD@gCJoFR}DT}C`@s@mBa@gAi@uBOmAAuB?y@f@WXFxEvCz@b@a@|BMz@Ct@FnABXpBKbCIxCMdHQh@AFSjBkG??lCsJ`@}AAMd@}A??x@qCrCiIt@iB??lI_R??fFgLlByD??rImR??zIsR??zIqR?@pCeGr@gBV}@hAoF??h@}BTuA\\mC~AuNJB??KCrA_MDy@@sE????FcIjCd@??RHL?`@IVMLOJUJWHc@??JsAvATxB`@??|AZ",
|
||||||
|
"color": "#6495ED",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 10,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"name": "Main Street Inbound",
|
||||||
|
"abbr": "MS I",
|
||||||
|
"stops": [
|
||||||
|
42,
|
||||||
|
43,
|
||||||
|
44,
|
||||||
|
45,
|
||||||
|
46,
|
||||||
|
47,
|
||||||
|
48,
|
||||||
|
49,
|
||||||
|
50,
|
||||||
|
51,
|
||||||
|
79,
|
||||||
|
155,
|
||||||
|
80,
|
||||||
|
81,
|
||||||
|
82,
|
||||||
|
83,
|
||||||
|
84,
|
||||||
|
85,
|
||||||
|
32,
|
||||||
|
61,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "wv|_G~~inMyCk@??KC_@SwB_@A?}@OSFKtAI`@Uf@UV??IDg@NQHmB]??_@IItL???`BEx@c@vDM~A??sAhLGAF@y@vH]zB?A}BpK??W~@Wr@eBvD??yCtG??{CtG??yCtG??mAjC??uAxC?@gDnH??gDpH??gDpH??uAnCuA|C??gDpH??gDrH??gDnH??]|@_C~G??{@jCw@lCGDW~@??wCjK??oBjH{IRuH^iD`@y@Hg@qAk@{Ac@}AQ{@IcAAmA@kA\\WNGd@RtD`Cd@Vh@TShA]dBAn@BjAHp@zEYtGU`FIs@vB??sA|EGZs@jC?AAHfEjC?T??@pCDhD??RzH??@t@}DP??yFX??}BJIF?fA??DjFJ~F??@x@FXFtH??DzL??@dGDxBKl@?H??BzCTlG??DlA@`GdA@??lBAf@QLIVYP]Po@VsA??TcAZq@RSRMTGZC??XCNGpCE@?t@Ij@KbA]\\Q??`EuB??hAk@lCqA??`@Op@Ql@IfAA|@F??t@LRFhAj@f@\\t@t@??rBrB\\V|Av@??r@\\NLfFvB??dHvC??rErB??pB|@lBr@??THfBVxAP??xCTjAB??vCD~AH??pFN??tCJTJPNLRJX??BTBh@Gf@GRU^MHYJUBOA??SGWSS_@Ig@AiA??CKPwK??JaL??@IPo@DIRMXGjCR??dBT|@RdBf@??|@d@nA`A??ZVNT~@nC??Xz@V`CXhB??`AzDFb@^|@??Vf@v@|@x@j@~@\\??b@HfAFl@?h@CNERK??n@Q^Wx@cAh@}@??Vo@Tq@\\uA^aBDY@WL}@??TkBRyCLqC??@M????`@UDM@U???oCCeAC]_@BK@M~@?Z",
|
||||||
|
"color": "#6495ED",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 11,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"name": "Riviera Ridge - Town Square Mall",
|
||||||
|
"abbr": "RRT",
|
||||||
|
"stops": [
|
||||||
|
118,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
119,
|
||||||
|
120,
|
||||||
|
121,
|
||||||
|
122,
|
||||||
|
123,
|
||||||
|
124,
|
||||||
|
125,
|
||||||
|
126,
|
||||||
|
127,
|
||||||
|
128,
|
||||||
|
32,
|
||||||
|
61,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "me{_GbysnMX|@d@vBRzAFxA??JdAFvC@nCC^??K~BSxCUjBM|@AV??e@zBi@rB_@~@Yf@m@x@??]b@_@Vo@PSCy@?aAAq@M??aAUa@Q_@UKMeBbD??MTcBfBy@l@mAr@??uBv@mAXsB`@??g@JSRUb@o@Ka@Ek@@iBF??sAF_@?MjM??EzBe@rL??q@nM??q@vM??KfDGvE??CnI??CfI??EhN?AFvFLrF??D`AXD~BInGa@??rJg@??rCOBbB??VzHPjI??NbJRhD??ZrEyEZ??UBoAZiF~B??}GzC??yC|AwA~@??{AlANbC??bA`P??H~AD^f@fA??F\\m@\\SN??CBFj@Bf@Mf@Kf@EXJX??FZ\\|@??t@fB??RXFDf@GNCIsA??OaC??OcC??MsB??C_@bAO??~AK??|AO??dAKLAMuB??OaC??QgC??OgC??QeC??OgC??OeC?AQcC??OoB??OuB??UkC??U{C??W{C??U{C??U{C??W{C?@U{C??W{C??U{C??Q}C?@MyC??GqC??AmC??CmC??@mC??@oC??BmC???mC??@kC???_B??@qB??@oB??@}B??DaC?@F}B@?A?J_C??JaC??L_C??L_C??J_C??L_C??LaC??L_C??J_C??H_C??J}B??F_C??F}B??D_C??BaC??BmB`AE??jBIA?|@Ah@F??f@HTc@XU??fB]??bB_@??~Ag@??l@Wh@[??b@Wl@c@??jAmA??b@c@j@aA??v@}Ax@j@z@Z??f@Jj@D??h@Br@C??TANERK^I@?LG^Wn@u@??Zg@l@oA??Vu@\\sA?A^aBDg@??VmB??RyB??NwB??JsB??@Y`@UDM@U???cC??EyAAU[BM@A?????Ij@Cf@",
|
||||||
|
"color": "#E37383",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 12,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"name": "Oakdale Commons",
|
||||||
|
"abbr": "OC",
|
||||||
|
"stops": [
|
||||||
|
118,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
129,
|
||||||
|
130,
|
||||||
|
32,
|
||||||
|
61,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "me{_GdysnMBFf@jBR~@NhAHz@Bv@H|@FpD??@tBO~CSxCUjB??M|@AVEX}@vDk@|AYf@k@x@??_@b@_@Vo@PSCy@?aAAgAS??mAa@_@UKMkA|Bc@r@??gBnBy@l@mAr@QH??cBl@mAXoB^??k@LSRUb@o@Ka@E{BF??mBHDuB??B}BTcAZ{@rBqC??Zc@L[FUDu@Ek@Og@W_@OKSIQCe@A??{FO??kGQ??{CM{@GsAQ??{@KmAYcBc@yAg@??iCcA{DaB??gIkD??mD}AqCqA??k@a@mDkDm@c@??kAk@eAW{@IeA???aANs@NmAd@qAp@??sGdD??g@To@Ru@Pw@HsAB??uG@??}DGyA???_CAyCDg@BA?iBHsARqAXo@R??q@TyB|@eClA??cHnD??kD`BuBz@??uExB]JYDG^EpA??EtB@tBA`E???hAaBB??Y@Ut@Uf@]n@??UZWXu@f@e@Py@NeER?O???qCEc@??ASEEe@???iBBED??CNCbA??B~B???DfDCz@E??hCMx@OJE??XKt@g@VYT[??\\o@Tg@Ts@??zBGNFv@@??N?r@RRBlAE??n@EE}B??M{G??G_CSa@WZ??y@dAKTEVB~@??J~F??DvBSCs@S??WOWUISKu@@mA??FkF??D}ATi@??d@w@xAo@??pCyA??vCwA??vCuA??vCuA??tCwA??lCoA??~@a@zAg@??x@Q~AS??pAKdCC??|DA??fE???dE???dE???z@Al@EfAU??|@YxBgA??nDkB??|BiAt@Y??l@Ol@IfAAn@D??bANZJ`Af@t@h@??zC|Cj@^??dBx@\\RNLnAh@??jDvA??xD`B??xD`B??bEhB??p@ZpBr@TD??pAPvBTA?|BPdBD??|BBvAH??dFN??~ABhAFTJRP??Tf@DXBh@Gf@Sd@WVYJ_@@??OAWOSWIQIg@?_AEU??HeF??HeF??DgF????DoCPo@DIRMXG\\B??rBNjAP??|Bf@`AZ??t@`@bBpA??V\\\\~@j@hBN`@BX??b@lDd@tB??d@hBBXHTb@bAV`@?Al@r@x@j@x@ZTFn@D??j@Dl@?h@Cl@U??d@MTOVW|@kA??n@uAf@cB??j@eCDY@WF]??ZkCLiB??ToE??ZOHIBa@?a@???kBCw@?@Cm@k@D????Ij@Ch@",
|
||||||
|
"color": "#FFC000",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 13,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"name": "Downtown Southside Outbound",
|
||||||
|
"abbr": "DSO",
|
||||||
|
"stops": [
|
||||||
|
136,
|
||||||
|
137,
|
||||||
|
1,
|
||||||
|
138,
|
||||||
|
139,
|
||||||
|
140,
|
||||||
|
141,
|
||||||
|
142,
|
||||||
|
143,
|
||||||
|
144,
|
||||||
|
145,
|
||||||
|
146,
|
||||||
|
147,
|
||||||
|
148,
|
||||||
|
154,
|
||||||
|
150,
|
||||||
|
151,
|
||||||
|
153,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"vType": "Bus",
|
||||||
|
"encLine": "ub{_GbitnM@?@VDTLZ?X@tBQtD??QbCUjBM|@AVEX}@vDk@|AYj@A?{@hAWVa@Tc@JSCy@?aAA_AQ_AWA?e@WYW??i@m@e@o@e@aAQMc@aBg@}BYyB??MmA@e@G_B??QuA@M@UAQMWKI??IEWDg@I??}@U}@[??_Cs@{@SoBYgCMe@_@M[??Kq@JmF??ToP??R{M??DsATgDZuC^gC??xBuL??fAeG??pAeHfC{N?AfAsGNgA??^gCz@sE??`@yB@A\\[^gA??n@kB??r@wBVo@j@iA??|BmE??`BkE??|BmG??fCaH??t@sBbB{F??xBsH??V}@\\gBLmAJoB?@TuD`@cE??|A_Q?A`@mEj@gF??V}C?@HsBMyEKaAUgAiAkD??mB{Go@qB??mE{M??gBsF?@}@qC??EOwCK??aDEuAG??[Gi@Sg@_@Y[Sc@Qc@gAsE??gCyL??i@uCk@eF??a@qEUoG??]mNK_C??mAuJqAkG??UmB_AaN??WiDuAcL??_AqH??Ky@mBp@??uFrB??uFrB??yGbC??eBr@s@`@}BzA??uEbDOH[F??e@LsEx@?rC@hBh@lFd@xEb@hCn@`D`@bCf@tCj@pCThAz@bF??TbB??BPEf@PpAjAnG??R`AJHNt@??ZfBb@jG??Dn@vAX??nB^??lB\\@?n@DzE???z@ATNO|B??IrA??CXR@jBZ??hDn@@?j@L",
|
||||||
|
"color": "#CECECE",
|
||||||
|
"type": "INBOUND",
|
||||||
|
"order": 15,
|
||||||
|
"showDirection": false,
|
||||||
|
"showPlatform": false,
|
||||||
|
"showScheduleNumber": 1,
|
||||||
|
"useCustomShape": 0,
|
||||||
|
"showVehicleCapacity": true
|
||||||
|
}
|
||||||
|
]
|
2082
src/client/data/stops.json
Normal file
2082
src/client/data/stops.json
Normal file
File diff suppressed because it is too large
Load diff
4
src/client/lib/data.dart
Normal file
4
src/client/lib/data.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export "src/data/utils.dart";
|
||||||
|
export "src/data/trip.dart";
|
||||||
|
export "src/data/path.dart";
|
||||||
|
export "src/data/stop.dart";
|
27
src/client/lib/main.dart
Normal file
27
src/client/lib/main.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
import "package:client/models.dart";
|
||||||
|
import "package:client/pages.dart";
|
||||||
|
import "package:client/services.dart";
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
await services.init();
|
||||||
|
await models.init();
|
||||||
|
await models.initFromOthers();
|
||||||
|
runApp(const ClientApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main app widget.
|
||||||
|
class ClientApp extends StatelessWidget {
|
||||||
|
/// A const constructor.
|
||||||
|
const ClientApp();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
|
title: "Flutter Demo",
|
||||||
|
theme: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
routerConfig: router,
|
||||||
|
);
|
||||||
|
}
|
31
src/client/lib/models.dart
Normal file
31
src/client/lib/models.dart
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import "src/models/model.dart";
|
||||||
|
|
||||||
|
export "src/models/model.dart";
|
||||||
|
|
||||||
|
/// A [DataModel] to manage all other data models.
|
||||||
|
class Models extends DataModel {
|
||||||
|
/// Prevents other instances of this class from being created.
|
||||||
|
Models._();
|
||||||
|
|
||||||
|
// List your models here
|
||||||
|
|
||||||
|
/// A list of all models to manage.
|
||||||
|
List<DataModel> get models => [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async {
|
||||||
|
for (final model in models) {
|
||||||
|
await model.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> initFromOthers() async {
|
||||||
|
for (final model in models) {
|
||||||
|
await model.initFromOthers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The global data model singleton.
|
||||||
|
final models = Models._();
|
22
src/client/lib/pages.dart
Normal file
22
src/client/lib/pages.dart
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
export "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
import "src/pages/home.dart";
|
||||||
|
|
||||||
|
/// Contains all the routes for this app.
|
||||||
|
class Routes {
|
||||||
|
/// The route for the home page.
|
||||||
|
static const home = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The router for the app.
|
||||||
|
final router = GoRouter(
|
||||||
|
initialLocation: Routes.home,
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.home,
|
||||||
|
name: Routes.home,
|
||||||
|
builder: (_, __) => HomePage(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
27
src/client/lib/services.dart
Normal file
27
src/client/lib/services.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/// Defines and manages the different services used by the app.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import "src/services/api.dart";
|
||||||
|
import "src/services/service.dart";
|
||||||
|
|
||||||
|
/// A [Service] that manages all other services used by the app.
|
||||||
|
class Services extends Service {
|
||||||
|
/// Prevents other instances of this class from being created.
|
||||||
|
Services._();
|
||||||
|
|
||||||
|
// Define your services here
|
||||||
|
final api = ApiService();
|
||||||
|
|
||||||
|
/// The different services to initialize, in this order.
|
||||||
|
List<Service> get services => [api];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async {
|
||||||
|
for (final service in services) {
|
||||||
|
await service.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The global services object.
|
||||||
|
final services = Services._();
|
15
src/client/lib/src/data/path.dart
Normal file
15
src/client/lib/src/data/path.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import "trip.dart";
|
||||||
|
import "utils.dart";
|
||||||
|
|
||||||
|
class PathStep {
|
||||||
|
final Trip trip;
|
||||||
|
final TripStop enter;
|
||||||
|
final TripStop exit;
|
||||||
|
|
||||||
|
PathStep.fromJson(Json json) :
|
||||||
|
trip = Trip.fromJson(json["trip"]),
|
||||||
|
enter = TripStop.fromJson(json["enter_bus"]),
|
||||||
|
exit = TripStop.fromJson(json["exit_bus"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef Path = List<PathStep>;
|
13
src/client/lib/src/data/stop.dart
Normal file
13
src/client/lib/src/data/stop.dart
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import "trip.dart";
|
||||||
|
import "utils.dart";
|
||||||
|
|
||||||
|
class Stop {
|
||||||
|
final double latitude;
|
||||||
|
final double longitude;
|
||||||
|
final List<TripID> trips;
|
||||||
|
|
||||||
|
Stop.fromJson(Json json) :
|
||||||
|
latitude = json["latitude"],
|
||||||
|
longitude = json["longitude"],
|
||||||
|
trips = json["trips"];
|
||||||
|
}
|
71
src/client/lib/src/data/trip.dart
Normal file
71
src/client/lib/src/data/trip.dart
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import "utils.dart";
|
||||||
|
|
||||||
|
import "package:flutter/material.dart" show TimeOfDay;
|
||||||
|
|
||||||
|
extension type TripID(String id) { }
|
||||||
|
extension type StopID(String id) { }
|
||||||
|
|
||||||
|
class Trip {
|
||||||
|
final List<TripStop> stops;
|
||||||
|
final String path;
|
||||||
|
final String name;
|
||||||
|
final String abbrev;
|
||||||
|
final String polyline;
|
||||||
|
|
||||||
|
const Trip({
|
||||||
|
required this.stops,
|
||||||
|
required this.path,
|
||||||
|
required this.name,
|
||||||
|
required this.abbrev,
|
||||||
|
required this.polyline,
|
||||||
|
});
|
||||||
|
|
||||||
|
Trip.fromJson(Json json) :
|
||||||
|
stops = [
|
||||||
|
for (final stop in json["stops"])
|
||||||
|
TripStop.fromJson(stop),
|
||||||
|
],
|
||||||
|
path = json["path"],
|
||||||
|
name = json["name"],
|
||||||
|
abbrev = json["abbrev"],
|
||||||
|
polyline = json["polyline"];
|
||||||
|
}
|
||||||
|
|
||||||
|
class TripStop {
|
||||||
|
final double lat;
|
||||||
|
final double long;
|
||||||
|
final List<ScheduledTime> times;
|
||||||
|
|
||||||
|
const TripStop({
|
||||||
|
required this.lat,
|
||||||
|
required this.long,
|
||||||
|
required this.times,
|
||||||
|
});
|
||||||
|
|
||||||
|
TripStop.fromJson(Json json) :
|
||||||
|
lat = json["latitude"],
|
||||||
|
long = json["longitude"],
|
||||||
|
times = [
|
||||||
|
for (final time in json["times"])
|
||||||
|
ScheduledTime.fromJson(time),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScheduledTime {
|
||||||
|
static const weekdays = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
||||||
|
|
||||||
|
final String weekday;
|
||||||
|
final TimeOfDay timeOfDay;
|
||||||
|
|
||||||
|
const ScheduledTime({
|
||||||
|
required this.weekday,
|
||||||
|
required this.timeOfDay,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ScheduledTime.fromJson(Json json) {
|
||||||
|
final weekday = json["dotw"];
|
||||||
|
final [hour, minute] = (json["time"] as String).split(":");
|
||||||
|
final timeOfDay = TimeOfDay(hour: int.parse(hour), minute: int.parse(minute));
|
||||||
|
return ScheduledTime(weekday: weekday, timeOfDay: timeOfDay);
|
||||||
|
}
|
||||||
|
}
|
30
src/client/lib/src/data/utils.dart
Normal file
30
src/client/lib/src/data/utils.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/// A JSON object
|
||||||
|
typedef Json = Map<String, dynamic>;
|
||||||
|
|
||||||
|
typedef FromJson<T> = T Function(Json);
|
||||||
|
|
||||||
|
typedef Coordinates = ({double lat, double long});
|
||||||
|
|
||||||
|
/// Utils on [Map].
|
||||||
|
extension MapUtils<K, V> on Map<K, V> {
|
||||||
|
/// Gets all the keys and values as 2-element records.
|
||||||
|
Iterable<(K, V)> get records => entries.map((entry) => (entry.key, entry.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zips two lists, like Python
|
||||||
|
Iterable<(E1, E2)> zip<E1, E2>(List<E1> list1, List<E2> list2) sync* {
|
||||||
|
if (list1.length != list2.length) throw ArgumentError("Trying to zip lists of different lengths");
|
||||||
|
for (var index = 0; index < list1.length; index++) {
|
||||||
|
yield (list1[index], list2[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extensions on lists
|
||||||
|
extension ListUtils<E> on List<E> {
|
||||||
|
/// Iterates over a pair of indexes and elements, like Python
|
||||||
|
Iterable<(int, E)> get enumerate sync* {
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
yield (i, this[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/client/lib/src/models/model.dart
Normal file
17
src/client/lib/src/models/model.dart
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import "package:flutter/foundation.dart";
|
||||||
|
|
||||||
|
/// A model containing data needed throughout the app.
|
||||||
|
///
|
||||||
|
/// This model may need to be initialized, so [init] should be called before using it. This model
|
||||||
|
/// should also be held as a singleton in some global scope.
|
||||||
|
abstract class DataModel with ChangeNotifier {
|
||||||
|
/// Loads any data needed by the model.
|
||||||
|
///
|
||||||
|
/// This function must not depend on any other model.
|
||||||
|
Future<void> init();
|
||||||
|
|
||||||
|
/// Loads any data from other models.
|
||||||
|
///
|
||||||
|
/// At this point, all models have run [init], so it is safe to use other models.
|
||||||
|
Future<void> initFromOthers() async { }
|
||||||
|
}
|
86
src/client/lib/src/pages/home.dart
Normal file
86
src/client/lib/src/pages/home.dart
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import "package:client/data.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
import "package:client/view_models.dart";
|
||||||
|
import "package:client/widgets.dart";
|
||||||
|
|
||||||
|
/// The home page.
|
||||||
|
class HomePage extends ReactiveWidget<HomeModel> {
|
||||||
|
@override
|
||||||
|
HomeModel createModel() => HomeModel();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, HomeModel model) => Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("Counter")),
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: model.startLatitudeController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "Start latitude",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: model.startLongitudeController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "Start longitude",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: model.endLatitudeController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "End latitude",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: model.endLongitudeController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "End Longitude",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
FilledButton(onPressed: model.search, child: const Text("Search for a path")),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (model.isLoading)
|
||||||
|
const LinearProgressIndicator()
|
||||||
|
else if (model.isSearching && model.path == null)
|
||||||
|
const Text("Could not connect to API")
|
||||||
|
else
|
||||||
|
for (final step in model.path ?? <PathStep>[]) Text(
|
||||||
|
"Get on at ${step.enter.lat}, ${step.enter.long}\n"
|
||||||
|
"Get off at ${step.exit.lat}, ${step.exit.long}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
53
src/client/lib/src/services/api.dart
Normal file
53
src/client/lib/src/services/api.dart
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import "dart:convert";
|
||||||
|
|
||||||
|
import "package:http/http.dart" as http;
|
||||||
|
|
||||||
|
import "package:client/data.dart";
|
||||||
|
|
||||||
|
import "service.dart";
|
||||||
|
|
||||||
|
class ApiService extends Service {
|
||||||
|
final client = http.Client();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async { }
|
||||||
|
|
||||||
|
Uri get _base => Uri(scheme: "https", host: "localhost", port: 5000);
|
||||||
|
|
||||||
|
Future<List<T>?> _getAll<T>(Uri uri, FromJson<T> fromJson) async {
|
||||||
|
try {
|
||||||
|
final response = await client.get(uri);
|
||||||
|
if (response.statusCode != 200) return null;
|
||||||
|
final json = jsonDecode(response.body) as List;
|
||||||
|
return [
|
||||||
|
for (final obj in json.cast<Json>())
|
||||||
|
fromJson(obj),
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Trip>?> getTrips() => _getAll(
|
||||||
|
_base.replace(path: "/trips"),
|
||||||
|
Trip.fromJson,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<Path?> getPath({
|
||||||
|
required Coordinates start,
|
||||||
|
required Coordinates end,
|
||||||
|
}) => _getAll(
|
||||||
|
_base.replace(
|
||||||
|
path: "/path",
|
||||||
|
).replace(
|
||||||
|
queryParameters: {
|
||||||
|
"start_lat": start.lat.toString(),
|
||||||
|
"start_lon": start.long.toString(),
|
||||||
|
"end_lat": end.lat.toString(),
|
||||||
|
"end_lon": end.long.toString(),
|
||||||
|
"time": DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PathStep.fromJson,
|
||||||
|
);
|
||||||
|
}
|
7
src/client/lib/src/services/service.dart
Normal file
7
src/client/lib/src/services/service.dart
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/// Defines a service: a singleton object to manage a resource.
|
||||||
|
abstract class Service {
|
||||||
|
/// Initializes any resources needed by the service.
|
||||||
|
///
|
||||||
|
/// This is guaranteed to be called before any other methods.
|
||||||
|
Future<void> init();
|
||||||
|
}
|
34
src/client/lib/src/view_models/home.dart
Normal file
34
src/client/lib/src/view_models/home.dart
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import "package:client/data.dart";
|
||||||
|
import "package:client/services.dart";
|
||||||
|
import "package:flutter/widgets.dart" hide Path;
|
||||||
|
|
||||||
|
import "view_model.dart";
|
||||||
|
|
||||||
|
/// The view model for the home page.
|
||||||
|
class HomeModel extends ViewModel {
|
||||||
|
final startLatitudeController = TextEditingController();
|
||||||
|
final startLongitudeController = TextEditingController();
|
||||||
|
final endLatitudeController = TextEditingController();
|
||||||
|
final endLongitudeController = TextEditingController();
|
||||||
|
|
||||||
|
Path? path;
|
||||||
|
|
||||||
|
double? get startLatitude => double.tryParse(startLatitudeController.text);
|
||||||
|
double? get startLongitude => double.tryParse(startLongitudeController.text);
|
||||||
|
double? get endLatitude => double.tryParse(endLatitudeController.text);
|
||||||
|
double? get endLongitude => double.tryParse(endLongitudeController.text);
|
||||||
|
|
||||||
|
bool isSearching = false;
|
||||||
|
|
||||||
|
Future<void> search() async {
|
||||||
|
final start = (lat: startLatitude, long: startLongitude);
|
||||||
|
final end = (lat: endLatitude, long: endLongitude);
|
||||||
|
if (start.lat == null || start.long == null) return;
|
||||||
|
if (end.lat == null || end.long == null) return;
|
||||||
|
isSearching = true;
|
||||||
|
isLoading = true;
|
||||||
|
path = await services.api.getPath(start: start as Coordinates, end: end as Coordinates);
|
||||||
|
if (path != null) isSearching = false;
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
54
src/client/lib/src/view_models/view_model.dart
Normal file
54
src/client/lib/src/view_models/view_model.dart
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import "package:flutter/foundation.dart";
|
||||||
|
|
||||||
|
/// A model to load and manage state needed by any piece of UI.
|
||||||
|
///
|
||||||
|
/// [init] is called right away it is *not* awaited. Use [isLoading] and
|
||||||
|
/// [errorText] to convey progress to the user. This allows the UI to load immediately.
|
||||||
|
abstract class ViewModel with ChangeNotifier {
|
||||||
|
/// Calls [init] right away but does not await it.
|
||||||
|
ViewModel() {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override this method to initializes any data needed by the model.
|
||||||
|
Future<void> init() async {}
|
||||||
|
|
||||||
|
/// Whether this model is currently loading data. Setting this updates the UI.
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
set isLoading(bool value) {
|
||||||
|
_isLoading = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this model has encountered an error. Setting this updates the UI.
|
||||||
|
String? _errorText;
|
||||||
|
String? get errorText => _errorText;
|
||||||
|
set errorText(String? value) {
|
||||||
|
_errorText = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wether this model is still attached to a widget.
|
||||||
|
bool _isMounted = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void notifyListeners() {
|
||||||
|
if (_isMounted) super.notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_isMounted = false;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A model to build a value from the UI.
|
||||||
|
abstract class BuilderModel<T> extends ViewModel {
|
||||||
|
/// The value being edited.
|
||||||
|
T get value;
|
||||||
|
|
||||||
|
/// Whether the [value] is ready to be accessed.
|
||||||
|
bool get isReady;
|
||||||
|
}
|
80
src/client/lib/src/widgets/generic/reactive_widget.dart
Normal file
80
src/client/lib/src/widgets/generic/reactive_widget.dart
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
abstract class ReactiveWidgetInterface<T extends ChangeNotifier> extends StatefulWidget {
|
||||||
|
const ReactiveWidgetInterface();
|
||||||
|
|
||||||
|
/// A function to create or find the model. This function will only be called once.
|
||||||
|
T createModel();
|
||||||
|
|
||||||
|
/// Whether the view model should be disposed or not.
|
||||||
|
bool get shouldDispose;
|
||||||
|
|
||||||
|
/// Builds the UI according to the state in [model].
|
||||||
|
Widget build(BuildContext context, T model);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ReactiveWidgetState createState() => ReactiveWidgetState<T>();
|
||||||
|
|
||||||
|
/// This function gives you an opportunity to update the view model when the widget updates.
|
||||||
|
///
|
||||||
|
/// For more details, see [State.didUpdateWidget].
|
||||||
|
void didUpdateWidget(covariant ReactiveWidgetInterface<T> oldWidget, T model) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that creates, subscribes to, and disposes of a [ChangeNotifier].
|
||||||
|
abstract class ReactiveWidget<T extends ChangeNotifier> extends ReactiveWidgetInterface<T> {
|
||||||
|
/// A const constructor.
|
||||||
|
const ReactiveWidget();
|
||||||
|
|
||||||
|
@override
|
||||||
|
T createModel();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get shouldDispose => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [ReactiveWidget] that works with a reusable [ChangeNotifier].
|
||||||
|
abstract class ReusableReactiveWidget<T extends ChangeNotifier> extends ReactiveWidgetInterface<T> {
|
||||||
|
/// The model to listen to.
|
||||||
|
final T model;
|
||||||
|
/// Creates a widget that listens to a view model.
|
||||||
|
const ReusableReactiveWidget(this.model);
|
||||||
|
|
||||||
|
@override
|
||||||
|
T createModel() => model;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get shouldDispose => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A state for [ReactiveWidget] that manages the [model].
|
||||||
|
class ReactiveWidgetState<T extends ChangeNotifier> extends State<ReactiveWidgetInterface<T>>{
|
||||||
|
/// The model to listen to.
|
||||||
|
late final T model;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
model = widget.createModel();
|
||||||
|
model.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
model.removeListener(listener);
|
||||||
|
if (widget.shouldDispose) model.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant ReactiveWidgetInterface<T> oldWidget) {
|
||||||
|
widget.didUpdateWidget(oldWidget, model);
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the UI when [model] updates.
|
||||||
|
void listener() => setState(() {});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => widget.build(context, model);
|
||||||
|
}
|
3
src/client/lib/view_models.dart
Normal file
3
src/client/lib/view_models.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export "src/view_models/view_model.dart";
|
||||||
|
|
||||||
|
export "src/view_models/home.dart";
|
20
src/client/lib/widgets.dart
Normal file
20
src/client/lib/widgets.dart
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
export "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
export "src/widgets/generic/reactive_widget.dart";
|
||||||
|
|
||||||
|
/// Helpful methods on [BuildContext].
|
||||||
|
extension ContextUtils on BuildContext {
|
||||||
|
/// Gets the app's color scheme.
|
||||||
|
ColorScheme get colorScheme => Theme.of(this).colorScheme;
|
||||||
|
|
||||||
|
/// Gets the app's text theme.
|
||||||
|
TextTheme get textTheme => Theme.of(this).textTheme;
|
||||||
|
|
||||||
|
/// Formats a date according to the user's locale.
|
||||||
|
String formatDate(DateTime date) => MaterialLocalizations.of(this).formatCompactDate(date);
|
||||||
|
|
||||||
|
/// Formats a time according to the user's locale.
|
||||||
|
String formatTime(DateTime time) => MaterialLocalizations.of(this).formatTimeOfDay(TimeOfDay.fromDateTime(time));
|
||||||
|
}
|
266
src/client/pubspec.lock
Normal file
266
src/client/pubspec.lock
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
go_router:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: go_router
|
||||||
|
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.8.1"
|
||||||
|
html:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.5"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.0.9"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.9"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.17"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.16.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.1"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.4"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
very_good_analysis:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: very_good_analysis
|
||||||
|
sha256: "1fb637c0022034b1f19ea2acb42a3603cbd8314a470646a59a2fb01f5f3a8629"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.0.0"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.7.0-0 <4.0.0"
|
||||||
|
flutter: ">=3.22.0"
|
22
src/client/pubspec.yaml
Normal file
22
src/client/pubspec.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
name: client
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
version: 0.1.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.5.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
go_router: ^14.2.7
|
||||||
|
html: ^0.15.5
|
||||||
|
http: ^1.3.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
very_good_analysis: ^6.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
30
src/client/test/widget_test.dart
Normal file
30
src/client/test/widget_test.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// This is a basic Flutter widget test.
|
||||||
|
//
|
||||||
|
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||||
|
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||||
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:client/main.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
|
// Build our app and trigger a frame.
|
||||||
|
await tester.pumpWidget(const MyApp());
|
||||||
|
|
||||||
|
// Verify that our counter starts at 0.
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
expect(find.text('1'), findsNothing);
|
||||||
|
|
||||||
|
// Tap the '+' icon and trigger a frame.
|
||||||
|
await tester.tap(find.byIcon(Icons.add));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Verify that our counter has incremented.
|
||||||
|
expect(find.text('0'), findsNothing);
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
BIN
src/client/web/favicon.png
Normal file
BIN
src/client/web/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 917 B |
BIN
src/client/web/icons/Icon-192.png
Normal file
BIN
src/client/web/icons/Icon-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
src/client/web/icons/Icon-512.png
Normal file
BIN
src/client/web/icons/Icon-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
BIN
src/client/web/icons/Icon-maskable-192.png
Normal file
BIN
src/client/web/icons/Icon-maskable-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
BIN
src/client/web/icons/Icon-maskable-512.png
Normal file
BIN
src/client/web/icons/Icon-maskable-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
38
src/client/web/index.html
Normal file
38
src/client/web/index.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--
|
||||||
|
If you are serving your web app in a path other than the root, change the
|
||||||
|
href value below to reflect the base path you are serving from.
|
||||||
|
|
||||||
|
The path provided below has to start and end with a slash "/" in order for
|
||||||
|
it to work correctly.
|
||||||
|
|
||||||
|
For more details:
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
|
|
||||||
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
|
the `--base-href` argument provided to `flutter build`.
|
||||||
|
-->
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="A new Flutter project.">
|
||||||
|
|
||||||
|
<!-- iOS meta tags & icons -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="client">
|
||||||
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
|
<title>client</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
35
src/client/web/manifest.json
Normal file
35
src/client/web/manifest.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"short_name": "client",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0175C2",
|
||||||
|
"theme_color": "#0175C2",
|
||||||
|
"description": "A new Flutter project.",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
17
src/client/windows/.gitignore
vendored
Normal file
17
src/client/windows/.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
flutter/ephemeral/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
108
src/client/windows/CMakeLists.txt
Normal file
108
src/client/windows/CMakeLists.txt
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(client LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "client")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(VERSION 3.14...3.25)
|
||||||
|
|
||||||
|
# Define build configuration option.
|
||||||
|
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||||
|
if(IS_MULTICONFIG)
|
||||||
|
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||||
|
CACHE STRING "" FORCE)
|
||||||
|
else()
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
# Define settings for the Profile build mode.
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||||
|
|
||||||
|
# Use Unicode for all projects.
|
||||||
|
add_definitions(-DUNICODE -D_UNICODE)
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||||
|
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||||
|
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# Support files are copied into place next to the executable, so that it can
|
||||||
|
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||||
|
# so that building and running from within Visual Studio will work.
|
||||||
|
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||||
|
# Make the "install" step default, as it's required to run.
|
||||||
|
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
CONFIGURATIONS Profile;Release
|
||||||
|
COMPONENT Runtime)
|
109
src/client/windows/flutter/CMakeLists.txt
Normal file
109
src/client/windows/flutter/CMakeLists.txt
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||||
|
|
||||||
|
# Set fallback configurations for older versions of the flutter tool.
|
||||||
|
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||||
|
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"flutter_export.h"
|
||||||
|
"flutter_windows.h"
|
||||||
|
"flutter_messenger.h"
|
||||||
|
"flutter_plugin_registrar.h"
|
||||||
|
"flutter_texture_registrar.h"
|
||||||
|
)
|
||||||
|
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Wrapper ===
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||||
|
"core_implementations.cc"
|
||||||
|
"standard_codec.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||||
|
"plugin_registrar.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||||
|
"flutter_engine.cc"
|
||||||
|
"flutter_view_controller.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
|
||||||
|
# Wrapper sources needed for a plugin.
|
||||||
|
add_library(flutter_wrapper_plugin STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_plugin)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
POSITION_INDEPENDENT_CODE ON)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden)
|
||||||
|
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||||
|
|
||||||
|
# Wrapper sources needed for the runner.
|
||||||
|
add_library(flutter_wrapper_app STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_app)
|
||||||
|
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_app PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||||
|
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
${PHONY_OUTPUT}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
11
src/client/windows/flutter/generated_plugin_registrant.cc
Normal file
11
src/client/windows/flutter/generated_plugin_registrant.cc
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
|
||||||
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
}
|
15
src/client/windows/flutter/generated_plugin_registrant.h
Normal file
15
src/client/windows/flutter/generated_plugin_registrant.h
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter/plugin_registry.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
23
src/client/windows/flutter/generated_plugins.cmake
Normal file
23
src/client/windows/flutter/generated_plugins.cmake
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||||
|
endforeach(ffi_plugin)
|
40
src/client/windows/runner/CMakeLists.txt
Normal file
40
src/client/windows/runner/CMakeLists.txt
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME in the
|
||||||
|
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||||
|
# work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME} WIN32
|
||||||
|
"flutter_window.cpp"
|
||||||
|
"main.cpp"
|
||||||
|
"utils.cpp"
|
||||||
|
"win32_window.cpp"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
"Runner.rc"
|
||||||
|
"runner.exe.manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the build version.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||||
|
|
||||||
|
# Disable Windows macros that collide with C++ standard library functions.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||||
|
|
||||||
|
# Add dependency libraries and include directories. Add any application-specific
|
||||||
|
# dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
121
src/client/windows/runner/Runner.rc
Normal file
121
src/client/windows/runner/Runner.rc
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
// Microsoft Visual C++ generated resource script.
|
||||||
|
//
|
||||||
|
#pragma code_page(65001)
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
#define APSTUDIO_READONLY_SYMBOLS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 2 resource.
|
||||||
|
//
|
||||||
|
#include "winres.h"
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// English (United States) resources
|
||||||
|
|
||||||
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// TEXTINCLUDE
|
||||||
|
//
|
||||||
|
|
||||||
|
1 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"resource.h\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
2 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"#include ""winres.h""\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
3 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // APSTUDIO_INVOKED
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Icon
|
||||||
|
//
|
||||||
|
|
||||||
|
// Icon with lowest ID value placed first to ensure application icon
|
||||||
|
// remains consistent on all systems.
|
||||||
|
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Version
|
||||||
|
//
|
||||||
|
|
||||||
|
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||||
|
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_NUMBER 1,0,0,0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(FLUTTER_VERSION)
|
||||||
|
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_STRING "1.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION VERSION_AS_NUMBER
|
||||||
|
PRODUCTVERSION VERSION_AS_NUMBER
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS VS_FF_DEBUG
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS VOS__WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904e4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "com.example" "\0"
|
||||||
|
VALUE "FileDescription", "client" "\0"
|
||||||
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
|
VALUE "InternalName", "client" "\0"
|
||||||
|
VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
|
||||||
|
VALUE "OriginalFilename", "client.exe" "\0"
|
||||||
|
VALUE "ProductName", "client" "\0"
|
||||||
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1252
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // English (United States) resources
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 3 resource.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#endif // not APSTUDIO_INVOKED
|
71
src/client/windows/runner/flutter_window.cpp
Normal file
71
src/client/windows/runner/flutter_window.cpp
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||||
|
: project_(project) {}
|
||||||
|
|
||||||
|
FlutterWindow::~FlutterWindow() {}
|
||||||
|
|
||||||
|
bool FlutterWindow::OnCreate() {
|
||||||
|
if (!Win32Window::OnCreate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
// The size here must match the window dimensions to avoid unnecessary surface
|
||||||
|
// creation / destruction in the startup path.
|
||||||
|
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||||
|
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||||
|
// Ensure that basic setup of the controller was successful.
|
||||||
|
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
|
||||||
|
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||||
|
this->Show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flutter can complete the first frame before the "show window" callback is
|
||||||
|
// registered. The following call ensures a frame is pending to ensure the
|
||||||
|
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||||||
|
flutter_controller_->ForceRedraw();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::OnDestroy() {
|
||||||
|
if (flutter_controller_) {
|
||||||
|
flutter_controller_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::OnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||||
|
if (flutter_controller_) {
|
||||||
|
std::optional<LRESULT> result =
|
||||||
|
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||||
|
lparam);
|
||||||
|
if (result) {
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case WM_FONTCHANGE:
|
||||||
|
flutter_controller_->engine()->ReloadSystemFonts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||||
|
}
|
33
src/client/windows/runner/flutter_window.h
Normal file
33
src/client/windows/runner/flutter_window.h
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
#define RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
|
||||||
|
#include <flutter/dart_project.h>
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
// A window that does nothing but host a Flutter view.
|
||||||
|
class FlutterWindow : public Win32Window {
|
||||||
|
public:
|
||||||
|
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
||||||
|
explicit FlutterWindow(const flutter::DartProject& project);
|
||||||
|
virtual ~FlutterWindow();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Win32Window:
|
||||||
|
bool OnCreate() override;
|
||||||
|
void OnDestroy() override;
|
||||||
|
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The project to run.
|
||||||
|
flutter::DartProject project_;
|
||||||
|
|
||||||
|
// The Flutter instance hosted by this window.
|
||||||
|
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_FLUTTER_WINDOW_H_
|
43
src/client/windows/runner/main.cpp
Normal file
43
src/client/windows/runner/main.cpp
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#include <flutter/dart_project.h>
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "flutter_window.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
|
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||||
|
// Attach to console when present (e.g., 'flutter run') or create a
|
||||||
|
// new console when running with a debugger.
|
||||||
|
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||||
|
CreateAndAttachConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize COM, so that it is available for use in the library and/or
|
||||||
|
// plugins.
|
||||||
|
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
|
|
||||||
|
flutter::DartProject project(L"data");
|
||||||
|
|
||||||
|
std::vector<std::string> command_line_arguments =
|
||||||
|
GetCommandLineArguments();
|
||||||
|
|
||||||
|
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||||
|
|
||||||
|
FlutterWindow window(project);
|
||||||
|
Win32Window::Point origin(10, 10);
|
||||||
|
Win32Window::Size size(1280, 720);
|
||||||
|
if (!window.Create(L"client", origin, size)) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
window.SetQuitOnClose(true);
|
||||||
|
|
||||||
|
::MSG msg;
|
||||||
|
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||||
|
::TranslateMessage(&msg);
|
||||||
|
::DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
::CoUninitialize();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
16
src/client/windows/runner/resource.h
Normal file
16
src/client/windows/runner/resource.h
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
//{{NO_DEPENDENCIES}}
|
||||||
|
// Microsoft Visual C++ generated include file.
|
||||||
|
// Used by Runner.rc
|
||||||
|
//
|
||||||
|
#define IDI_APP_ICON 101
|
||||||
|
|
||||||
|
// Next default values for new objects
|
||||||
|
//
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||||
|
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||||
|
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||||
|
#define _APS_NEXT_SYMED_VALUE 101
|
||||||
|
#endif
|
||||||
|
#endif
|
BIN
src/client/windows/runner/resources/app_icon.ico
Normal file
BIN
src/client/windows/runner/resources/app_icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
14
src/client/windows/runner/runner.exe.manifest
Normal file
14
src/client/windows/runner/runner.exe.manifest
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 and Windows 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
65
src/client/windows/runner/utils.cpp
Normal file
65
src/client/windows/runner/utils.cpp
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
#include <io.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
void CreateAndAttachConsole() {
|
||||||
|
if (::AllocConsole()) {
|
||||||
|
FILE *unused;
|
||||||
|
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
||||||
|
_dup2(_fileno(stdout), 1);
|
||||||
|
}
|
||||||
|
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
||||||
|
_dup2(_fileno(stdout), 2);
|
||||||
|
}
|
||||||
|
std::ios::sync_with_stdio();
|
||||||
|
FlutterDesktopResyncOutputStreams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> GetCommandLineArguments() {
|
||||||
|
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
||||||
|
int argc;
|
||||||
|
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
||||||
|
if (argv == nullptr) {
|
||||||
|
return std::vector<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> command_line_arguments;
|
||||||
|
|
||||||
|
// Skip the first argument as it's the binary name.
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
::LocalFree(argv);
|
||||||
|
|
||||||
|
return command_line_arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
||||||
|
if (utf16_string == nullptr) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
unsigned int target_length = ::WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||||
|
-1, nullptr, 0, nullptr, nullptr)
|
||||||
|
-1; // remove the trailing null character
|
||||||
|
int input_length = (int)wcslen(utf16_string);
|
||||||
|
std::string utf8_string;
|
||||||
|
if (target_length == 0 || target_length > utf8_string.max_size()) {
|
||||||
|
return utf8_string;
|
||||||
|
}
|
||||||
|
utf8_string.resize(target_length);
|
||||||
|
int converted_length = ::WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||||
|
input_length, utf8_string.data(), target_length, nullptr, nullptr);
|
||||||
|
if (converted_length == 0) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
return utf8_string;
|
||||||
|
}
|
19
src/client/windows/runner/utils.h
Normal file
19
src/client/windows/runner/utils.h
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#ifndef RUNNER_UTILS_H_
|
||||||
|
#define RUNNER_UTILS_H_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Creates a console for the process, and redirects stdout and stderr to
|
||||||
|
// it for both the runner and the Flutter library.
|
||||||
|
void CreateAndAttachConsole();
|
||||||
|
|
||||||
|
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
||||||
|
// encoded in UTF-8. Returns an empty std::string on failure.
|
||||||
|
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
||||||
|
|
||||||
|
// Gets the command line arguments passed in as a std::vector<std::string>,
|
||||||
|
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||||
|
std::vector<std::string> GetCommandLineArguments();
|
||||||
|
|
||||||
|
#endif // RUNNER_UTILS_H_
|
288
src/client/windows/runner/win32_window.cpp
Normal file
288
src/client/windows/runner/win32_window.cpp
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
#include <dwmapi.h>
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// Window attribute that enables dark mode window decorations.
|
||||||
|
///
|
||||||
|
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||||
|
/// version 10.0.22000.0.
|
||||||
|
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||||
|
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
|
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||||
|
#endif
|
||||||
|
|
||||||
|
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||||
|
|
||||||
|
/// Registry key for app theme preference.
|
||||||
|
///
|
||||||
|
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||||
|
/// value indicates apps should use light mode.
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||||
|
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||||
|
|
||||||
|
// The number of Win32Window objects that currently exist.
|
||||||
|
static int g_active_window_count = 0;
|
||||||
|
|
||||||
|
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||||
|
|
||||||
|
// Scale helper to convert logical scaler values to physical using passed in
|
||||||
|
// scale factor
|
||||||
|
int Scale(int source, double scale_factor) {
|
||||||
|
return static_cast<int>(source * scale_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||||
|
// This API is only needed for PerMonitor V1 awareness mode.
|
||||||
|
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||||
|
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||||
|
if (!user32_module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto enable_non_client_dpi_scaling =
|
||||||
|
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||||
|
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||||
|
if (enable_non_client_dpi_scaling != nullptr) {
|
||||||
|
enable_non_client_dpi_scaling(hwnd);
|
||||||
|
}
|
||||||
|
FreeLibrary(user32_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Manages the Win32Window's window class registration.
|
||||||
|
class WindowClassRegistrar {
|
||||||
|
public:
|
||||||
|
~WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
// Returns the singleton registrar instance.
|
||||||
|
static WindowClassRegistrar* GetInstance() {
|
||||||
|
if (!instance_) {
|
||||||
|
instance_ = new WindowClassRegistrar();
|
||||||
|
}
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the name of the window class, registering the class if it hasn't
|
||||||
|
// previously been registered.
|
||||||
|
const wchar_t* GetWindowClass();
|
||||||
|
|
||||||
|
// Unregisters the window class. Should only be called if there are no
|
||||||
|
// instances of the window.
|
||||||
|
void UnregisterWindowClass();
|
||||||
|
|
||||||
|
private:
|
||||||
|
WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
static WindowClassRegistrar* instance_;
|
||||||
|
|
||||||
|
bool class_registered_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||||
|
|
||||||
|
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||||
|
if (!class_registered_) {
|
||||||
|
WNDCLASS window_class{};
|
||||||
|
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
|
window_class.lpszClassName = kWindowClassName;
|
||||||
|
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
window_class.cbClsExtra = 0;
|
||||||
|
window_class.cbWndExtra = 0;
|
||||||
|
window_class.hInstance = GetModuleHandle(nullptr);
|
||||||
|
window_class.hIcon =
|
||||||
|
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||||
|
window_class.hbrBackground = 0;
|
||||||
|
window_class.lpszMenuName = nullptr;
|
||||||
|
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||||
|
RegisterClass(&window_class);
|
||||||
|
class_registered_ = true;
|
||||||
|
}
|
||||||
|
return kWindowClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||||
|
UnregisterClass(kWindowClassName, nullptr);
|
||||||
|
class_registered_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::Win32Window() {
|
||||||
|
++g_active_window_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::~Win32Window() {
|
||||||
|
--g_active_window_count;
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Create(const std::wstring& title,
|
||||||
|
const Point& origin,
|
||||||
|
const Size& size) {
|
||||||
|
Destroy();
|
||||||
|
|
||||||
|
const wchar_t* window_class =
|
||||||
|
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||||
|
|
||||||
|
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||||
|
static_cast<LONG>(origin.y)};
|
||||||
|
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||||
|
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||||
|
double scale_factor = dpi / 96.0;
|
||||||
|
|
||||||
|
HWND window = CreateWindow(
|
||||||
|
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||||
|
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||||
|
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||||
|
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||||
|
|
||||||
|
if (!window) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateTheme(window);
|
||||||
|
|
||||||
|
return OnCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Show() {
|
||||||
|
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||||
|
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||||
|
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||||
|
|
||||||
|
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||||
|
EnableFullDpiSupportIfAvailable(window);
|
||||||
|
that->window_handle_ = window;
|
||||||
|
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||||
|
return that->MessageHandler(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
Win32Window::MessageHandler(HWND hwnd,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
switch (message) {
|
||||||
|
case WM_DESTROY:
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
Destroy();
|
||||||
|
if (quit_on_close_) {
|
||||||
|
PostQuitMessage(0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DPICHANGED: {
|
||||||
|
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||||
|
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||||
|
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||||
|
|
||||||
|
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||||
|
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WM_SIZE: {
|
||||||
|
RECT rect = GetClientArea();
|
||||||
|
if (child_content_ != nullptr) {
|
||||||
|
// Size and position the child window.
|
||||||
|
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||||
|
rect.bottom - rect.top, TRUE);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
case WM_ACTIVATE:
|
||||||
|
if (child_content_ != nullptr) {
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||||
|
UpdateTheme(hwnd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::Destroy() {
|
||||||
|
OnDestroy();
|
||||||
|
|
||||||
|
if (window_handle_) {
|
||||||
|
DestroyWindow(window_handle_);
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
}
|
||||||
|
if (g_active_window_count == 0) {
|
||||||
|
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||||
|
return reinterpret_cast<Win32Window*>(
|
||||||
|
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetChildContent(HWND content) {
|
||||||
|
child_content_ = content;
|
||||||
|
SetParent(content, window_handle_);
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||||
|
frame.bottom - frame.top, true);
|
||||||
|
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT Win32Window::GetClientArea() {
|
||||||
|
RECT frame;
|
||||||
|
GetClientRect(window_handle_, &frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND Win32Window::GetHandle() {
|
||||||
|
return window_handle_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||||
|
quit_on_close_ = quit_on_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::OnCreate() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::OnDestroy() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::UpdateTheme(HWND const window) {
|
||||||
|
DWORD light_mode;
|
||||||
|
DWORD light_mode_size = sizeof(light_mode);
|
||||||
|
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||||
|
kGetPreferredBrightnessRegValue,
|
||||||
|
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||||
|
&light_mode_size);
|
||||||
|
|
||||||
|
if (result == ERROR_SUCCESS) {
|
||||||
|
BOOL enable_dark_mode = light_mode == 0;
|
||||||
|
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||||
|
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||||
|
}
|
||||||
|
}
|
102
src/client/windows/runner/win32_window.h
Normal file
102
src/client/windows/runner/win32_window.h
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||||
|
#define RUNNER_WIN32_WINDOW_H_
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||||
|
// inherited from by classes that wish to specialize with custom
|
||||||
|
// rendering and input handling
|
||||||
|
class Win32Window {
|
||||||
|
public:
|
||||||
|
struct Point {
|
||||||
|
unsigned int x;
|
||||||
|
unsigned int y;
|
||||||
|
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Size {
|
||||||
|
unsigned int width;
|
||||||
|
unsigned int height;
|
||||||
|
Size(unsigned int width, unsigned int height)
|
||||||
|
: width(width), height(height) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Win32Window();
|
||||||
|
virtual ~Win32Window();
|
||||||
|
|
||||||
|
// Creates a win32 window with |title| that is positioned and sized using
|
||||||
|
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||||
|
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||||
|
// consistent size this function will scale the inputted width and height as
|
||||||
|
// as appropriate for the default monitor. The window is invisible until
|
||||||
|
// |Show| is called. Returns true if the window was created successfully.
|
||||||
|
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||||
|
|
||||||
|
// Show the current window. Returns true if the window was successfully shown.
|
||||||
|
bool Show();
|
||||||
|
|
||||||
|
// Release OS resources associated with window.
|
||||||
|
void Destroy();
|
||||||
|
|
||||||
|
// Inserts |content| into the window tree.
|
||||||
|
void SetChildContent(HWND content);
|
||||||
|
|
||||||
|
// Returns the backing Window handle to enable clients to set icon and other
|
||||||
|
// window properties. Returns nullptr if the window has been destroyed.
|
||||||
|
HWND GetHandle();
|
||||||
|
|
||||||
|
// If true, closing this window will quit the application.
|
||||||
|
void SetQuitOnClose(bool quit_on_close);
|
||||||
|
|
||||||
|
// Return a RECT representing the bounds of the current client area.
|
||||||
|
RECT GetClientArea();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Processes and route salient window messages for mouse handling,
|
||||||
|
// size change and DPI. Delegates handling of these to member overloads that
|
||||||
|
// inheriting classes can handle.
|
||||||
|
virtual LRESULT MessageHandler(HWND window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Called when CreateAndShow is called, allowing subclass window-related
|
||||||
|
// setup. Subclasses should return false if setup fails.
|
||||||
|
virtual bool OnCreate();
|
||||||
|
|
||||||
|
// Called when Destroy is called.
|
||||||
|
virtual void OnDestroy();
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class WindowClassRegistrar;
|
||||||
|
|
||||||
|
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||||
|
// is passed when the non-client area is being created and enables automatic
|
||||||
|
// non-client DPI scaling so that the non-client area automatically
|
||||||
|
// responds to changes in DPI. All other messages are handled by
|
||||||
|
// MessageHandler.
|
||||||
|
static LRESULT CALLBACK WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Retrieves a class instance pointer for |window|
|
||||||
|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||||
|
|
||||||
|
// Update the window frame's theme to match the system theme.
|
||||||
|
static void UpdateTheme(HWND const window);
|
||||||
|
|
||||||
|
bool quit_on_close_ = false;
|
||||||
|
|
||||||
|
// window handle for top level window.
|
||||||
|
HWND window_handle_ = nullptr;
|
||||||
|
|
||||||
|
// window handle for hosted content.
|
||||||
|
HWND child_content_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_WIN32_WINDOW_H_
|
Loading…
Reference in a new issue