add build step for flutter web, add persistent pagination in the traction list
Some checks failed
Release / meta (push) Successful in 14s
Release / web-build (push) Has been cancelled
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
Release / linux-build (push) Has been cancelled
Release / android-build (push) Has been cancelled
Some checks failed
Release / meta (push) Successful in 14s
Release / web-build (push) Has been cancelled
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
Release / linux-build (push) Has been cancelled
Release / android-build (push) Has been cancelled
This commit is contained in:
@@ -12,6 +12,7 @@ env:
|
|||||||
FLUTTER_VERSION: "3.38.5"
|
FLUTTER_VERSION: "3.38.5"
|
||||||
BUILD_WINDOWS: "false" # Windows build disabled (no runner available)
|
BUILD_WINDOWS: "false" # Windows build disabled (no runner available)
|
||||||
GITEA_BASE_URL: https://git.tgj.services
|
GITEA_BASE_URL: https://git.tgj.services
|
||||||
|
WEB_IMAGE: "git.tgj.services/petegregoryy/mileograph-web"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
meta:
|
meta:
|
||||||
@@ -255,6 +256,108 @@ jobs:
|
|||||||
name: linux-bundle
|
name: linux-bundle
|
||||||
path: app-linux-x64.tar.gz
|
path: app-linux-x64.tar.gz
|
||||||
|
|
||||||
|
web-build:
|
||||||
|
runs-on:
|
||||||
|
- mileograph
|
||||||
|
needs: meta
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install OS deps (Web)
|
||||||
|
run: |
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
SUDO="sudo"
|
||||||
|
else
|
||||||
|
SUDO=""
|
||||||
|
fi
|
||||||
|
$SUDO apt-get update
|
||||||
|
$SUDO apt-get install -y unzip xz-utils zip libstdc++6 liblzma-dev curl jq docker.io
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
$SUDO systemctl start docker 2>/dev/null || $SUDO service docker start 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Flutter SDK
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
FLUTTER_HOME="$HOME/flutter"
|
||||||
|
git config --global --add safe.directory "$FLUTTER_HOME" || true
|
||||||
|
if [ ! -x "$FLUTTER_HOME/bin/flutter" ]; then
|
||||||
|
rm -rf "$FLUTTER_HOME"
|
||||||
|
curl -fsSL -o /tmp/flutter.tar.xz "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz"
|
||||||
|
tar -C "$HOME" -xf /tmp/flutter.tar.xz
|
||||||
|
fi
|
||||||
|
echo "$FLUTTER_HOME/bin" >> "$GITHUB_PATH"
|
||||||
|
"$FLUTTER_HOME/bin/flutter" --version
|
||||||
|
|
||||||
|
- name: Allow all git directories (CI)
|
||||||
|
run: git config --global --add safe.directory '*'
|
||||||
|
|
||||||
|
- name: Set pub cache path
|
||||||
|
run: echo "PUB_CACHE=${GITHUB_WORKSPACE}/.pub-cache" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Flutter dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Enable Flutter web
|
||||||
|
run: flutter config --enable-web
|
||||||
|
|
||||||
|
- name: Build Flutter web (release)
|
||||||
|
run: |
|
||||||
|
flutter build web --release --base-href=/
|
||||||
|
tar -C build/web -czf app-web.tar.gz .
|
||||||
|
|
||||||
|
- name: Upload Web artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: web-build
|
||||||
|
path: app-web.tar.gz
|
||||||
|
|
||||||
|
- name: Compute web image tags
|
||||||
|
id: web_meta
|
||||||
|
env:
|
||||||
|
BASE_VERSION: ${{ needs.meta.outputs.base_version }}
|
||||||
|
DEV_SUFFIX: ${{ needs.meta.outputs.dev_suffix }}
|
||||||
|
run: |
|
||||||
|
IMAGE="${WEB_IMAGE}"
|
||||||
|
TAG=""
|
||||||
|
ALIAS=""
|
||||||
|
if [ "${GITHUB_REF}" = "refs/heads/dev" ]; then
|
||||||
|
TAG="${BASE_VERSION}${DEV_SUFFIX}"
|
||||||
|
ALIAS="dev"
|
||||||
|
elif [ "${GITHUB_REF}" = "refs/heads/master" ]; then
|
||||||
|
TAG="${BASE_VERSION}"
|
||||||
|
ALIAS="latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "alias=${ALIAS}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Login to registry
|
||||||
|
if: ${{ secrets.DOCKERHUB_TOKEN != '' && steps.web_meta.outputs.tag != '' }}
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "$REGISTRY_TOKEN" | docker login git.tgj.services -u petegregoryy --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push web image
|
||||||
|
if: ${{ secrets.DOCKERHUB_TOKEN != '' && steps.web_meta.outputs.tag != '' }}
|
||||||
|
env:
|
||||||
|
IMAGE: ${{ steps.web_meta.outputs.image }}
|
||||||
|
TAG: ${{ steps.web_meta.outputs.tag }}
|
||||||
|
ALIAS: ${{ steps.web_meta.outputs.alias }}
|
||||||
|
run: |
|
||||||
|
docker buildx create --name buildx --driver=docker-container --use || docker buildx use buildx
|
||||||
|
TAG_ARGS=(-t "${IMAGE}:${TAG}")
|
||||||
|
if [ -n "$ALIAS" ]; then
|
||||||
|
TAG_ARGS+=(-t "${IMAGE}:${ALIAS}")
|
||||||
|
fi
|
||||||
|
docker buildx build --builder buildx --platform linux/amd64 \
|
||||||
|
-f Dockerfile.web \
|
||||||
|
--push \
|
||||||
|
"${TAG_ARGS[@]}" .
|
||||||
|
|
||||||
release-dev:
|
release-dev:
|
||||||
runs-on:
|
runs-on:
|
||||||
- mileograph
|
- mileograph
|
||||||
@@ -262,6 +365,7 @@ jobs:
|
|||||||
- meta
|
- meta
|
||||||
- android-build
|
- android-build
|
||||||
- linux-build
|
- linux-build
|
||||||
|
- web-build
|
||||||
steps:
|
steps:
|
||||||
- name: Install jq
|
- name: Install jq
|
||||||
run: |
|
run: |
|
||||||
@@ -345,6 +449,7 @@ jobs:
|
|||||||
- meta
|
- meta
|
||||||
- android-build
|
- android-build
|
||||||
- linux-build
|
- linux-build
|
||||||
|
- web-build
|
||||||
steps:
|
steps:
|
||||||
- name: Install jq
|
- name: Install jq
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
10
Dockerfile.web
Normal file
10
Dockerfile.web
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
# Use a minimal Nginx image to serve the built Flutter web app.
|
||||||
|
# Assumes `flutter build web` has already populated build/web/ in the build context.
|
||||||
|
COPY deploy/web/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY build/web /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
28
deploy/web/nginx.conf
Normal file
28
deploy/web/nginx.conf
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
|
# Serve hashed assets aggressively; keep index/service worker cacheable but not immutable.
|
||||||
|
location /assets/ {
|
||||||
|
try_files $uri =404;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /icons/ {
|
||||||
|
try_files $uri =404;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /flutter_service_worker.js {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,12 +111,6 @@ class _LegCardState extends State<LegCard> {
|
|||||||
subtitle: LayoutBuilder(
|
subtitle: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final isWide = constraints.maxWidth > 520;
|
final isWide = constraints.maxWidth > 520;
|
||||||
final timeWidget = _timeWithDelay(
|
|
||||||
context,
|
|
||||||
leg.beginTime,
|
|
||||||
leg.beginDelayMinutes,
|
|
||||||
includeDate: widget.showDate,
|
|
||||||
);
|
|
||||||
final tractionWrap = !_expanded && leg.locos.isNotEmpty
|
final tractionWrap = !_expanded && leg.locos.isNotEmpty
|
||||||
? Wrap(
|
? Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
|
|||||||
@@ -128,10 +128,6 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double _manualMilesFromInput(DistanceUnitService units) {
|
|
||||||
return units.milesFromInput(_mileageController.text) ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _milesFromInputWithUnit(DistanceUnit unit) {
|
double _milesFromInputWithUnit(DistanceUnit unit) {
|
||||||
return DistanceFormatter(unit)
|
return DistanceFormatter(unit)
|
||||||
.parseInputMiles(_mileageController.text.trim()) ??
|
.parseInputMiles(_mileageController.text.trim()) ??
|
||||||
@@ -276,6 +272,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
final units = _distanceUnits(context);
|
final units = _distanceUnits(context);
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -1536,43 +1533,6 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _timeToggleBlock({
|
|
||||||
required String label,
|
|
||||||
required bool value,
|
|
||||||
required ValueChanged<bool?>? onChanged,
|
|
||||||
required String matchLabel,
|
|
||||||
required bool matchValue,
|
|
||||||
required ValueChanged<bool?>? onMatchChanged,
|
|
||||||
required bool showMatch,
|
|
||||||
Widget? picker,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
CheckboxListTile(
|
|
||||||
value: value,
|
|
||||||
onChanged: onChanged,
|
|
||||||
dense: true,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
title: Text(label),
|
|
||||||
),
|
|
||||||
if (showMatch)
|
|
||||||
CheckboxListTile(
|
|
||||||
value: matchValue,
|
|
||||||
onChanged: onMatchChanged,
|
|
||||||
dense: true,
|
|
||||||
contentPadding: const EdgeInsets.only(left: 12),
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
title: Text(matchLabel),
|
|
||||||
),
|
|
||||||
if (picker != null) ...[
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
picker,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UpperCaseTextFormatter extends TextInputFormatter {
|
class _UpperCaseTextFormatter extends TextInputFormatter {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ extension _NewEntrySubmitLogic on _NewEntryPageState {
|
|||||||
if (form == null) return;
|
if (form == null) return;
|
||||||
if (!form.validate()) return;
|
if (!form.validate()) return;
|
||||||
if (!await _validateRequiredFields()) return;
|
if (!await _validateRequiredFields()) return;
|
||||||
|
if (!mounted) return;
|
||||||
final routeStations = _routeResult?.calculatedRoute ?? [];
|
final routeStations = _routeResult?.calculatedRoute ?? [];
|
||||||
final startVal = _useManualMileage
|
final startVal = _useManualMileage
|
||||||
? _startController.text.trim()
|
? _startController.text.trim()
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ class _NewTractionPageState extends State<NewTractionPage> {
|
|||||||
'traction_motors': TextEditingController(),
|
'traction_motors': TextEditingController(),
|
||||||
'build_date': TextEditingController(),
|
'build_date': TextEditingController(),
|
||||||
};
|
};
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final data = context.read<DataService>();
|
||||||
|
if (data.locoClasses.isEmpty) {
|
||||||
|
data.fetchClassList();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -254,6 +261,10 @@ class _NewTractionPageState extends State<NewTractionPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isActive = _statusIsActive;
|
final isActive = _statusIsActive;
|
||||||
|
final data = context.watch<DataService>();
|
||||||
|
final classOptions = [...data.locoClasses]..sort(
|
||||||
|
(a, b) => a.toLowerCase().compareTo(b.toLowerCase()),
|
||||||
|
);
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final isNarrow = size.width < 720;
|
final isNarrow = size.width < 720;
|
||||||
final fieldWidth = isNarrow ? double.infinity : 340.0;
|
final fieldWidth = isNarrow ? double.infinity : 340.0;
|
||||||
@@ -269,6 +280,89 @@ class _NewTractionPageState extends State<NewTractionPage> {
|
|||||||
double? widthOverride,
|
double? widthOverride,
|
||||||
String? Function(String?)? validator,
|
String? Function(String?)? validator,
|
||||||
}) {
|
}) {
|
||||||
|
// Special autocomplete for class field using existing loco classes.
|
||||||
|
if (key == 'class' && classOptions.isNotEmpty) {
|
||||||
|
return SizedBox(
|
||||||
|
width: widthOverride ?? fieldWidth,
|
||||||
|
child: Autocomplete<String>(
|
||||||
|
optionsBuilder: (TextEditingValue value) {
|
||||||
|
final query = value.text.trim().toLowerCase();
|
||||||
|
if (query.isEmpty) return classOptions;
|
||||||
|
return classOptions.where(
|
||||||
|
(c) => c.toLowerCase().contains(query),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSelected: (selection) {
|
||||||
|
_controllers[key]?.text = selection;
|
||||||
|
_formKey.currentState?.validate();
|
||||||
|
},
|
||||||
|
fieldViewBuilder:
|
||||||
|
(context, textEditingController, focusNode, onFieldSubmitted) {
|
||||||
|
if (textEditingController.text != _controllers[key]?.text) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (textEditingController.text != _controllers[key]?.text) {
|
||||||
|
textEditingController.value =
|
||||||
|
_controllers[key]?.value ?? textEditingController.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return TextFormField(
|
||||||
|
controller: textEditingController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: required ? '$label *' : label,
|
||||||
|
helperText: helper,
|
||||||
|
suffixText: suffixText,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
maxLines: maxLines,
|
||||||
|
validator: (val) {
|
||||||
|
if (required && (val == null || val.trim().isEmpty)) {
|
||||||
|
return 'Required';
|
||||||
|
}
|
||||||
|
return validator?.call(val);
|
||||||
|
},
|
||||||
|
onChanged: (_) {
|
||||||
|
_controllers[key]?.text = textEditingController.text;
|
||||||
|
_formKey.currentState?.validate();
|
||||||
|
},
|
||||||
|
onFieldSubmitted: (_) => onFieldSubmitted(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
optionsViewBuilder: (context, onSelected, options) {
|
||||||
|
final opts = options.toList();
|
||||||
|
if (opts.isEmpty) return const SizedBox.shrink();
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Material(
|
||||||
|
elevation: 4,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: 280,
|
||||||
|
maxWidth: widthOverride ?? fieldWidth,
|
||||||
|
),
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: opts.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final option = opts[index];
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(option),
|
||||||
|
onTap: () => onSelected(option),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: widthOverride ?? fieldWidth,
|
width: widthOverride ?? fieldWidth,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class _TractionPageState extends State<TractionPage> {
|
|||||||
final Map<String, TextEditingController> _dynamicControllers = {};
|
final Map<String, TextEditingController> _dynamicControllers = {};
|
||||||
final Map<String, String?> _enumSelections = {};
|
final Map<String, String?> _enumSelections = {};
|
||||||
bool _restoredFromPrefs = false;
|
bool _restoredFromPrefs = false;
|
||||||
|
static const int _pageSize = 100;
|
||||||
|
int _lastTractionOffset = 0;
|
||||||
|
String? _lastQuerySignature;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -59,6 +62,9 @@ class _TractionPageState extends State<TractionPage> {
|
|||||||
Future<void> _initialLoad() async {
|
Future<void> _initialLoad() async {
|
||||||
final data = context.read<DataService>();
|
final data = context.read<DataService>();
|
||||||
await _restoreSearchState();
|
await _restoreSearchState();
|
||||||
|
if (_lastTractionOffset == 0 && data.traction.length > _pageSize) {
|
||||||
|
_lastTractionOffset = data.traction.length - _pageSize;
|
||||||
|
}
|
||||||
data.fetchClassList();
|
data.fetchClassList();
|
||||||
data.fetchEventFields();
|
data.fetchEventFields();
|
||||||
await _refreshTraction();
|
await _refreshTraction();
|
||||||
@@ -103,7 +109,29 @@ class _TractionPageState extends State<TractionPage> {
|
|||||||
dynamicFieldsUsed;
|
dynamicFieldsUsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshTraction({bool append = false}) async {
|
String _tractionQuerySignature(
|
||||||
|
Map<String, dynamic> filters,
|
||||||
|
bool hadOnly,
|
||||||
|
) {
|
||||||
|
final sortedKeys = filters.keys.toList()..sort();
|
||||||
|
final filterSignature = sortedKeys
|
||||||
|
.map((key) => '$key=${filters[key]}')
|
||||||
|
.join('|');
|
||||||
|
final classQuery = (_selectedClass ?? _classController.text).trim();
|
||||||
|
return [
|
||||||
|
'class=$classQuery',
|
||||||
|
'number=${_numberController.text.trim()}',
|
||||||
|
'name=${_nameController.text.trim()}',
|
||||||
|
'mileageFirst=$_mileageFirst',
|
||||||
|
'hadOnly=$hadOnly',
|
||||||
|
'filters=$filterSignature',
|
||||||
|
].join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshTraction({
|
||||||
|
bool append = false,
|
||||||
|
bool preservePosition = true,
|
||||||
|
}) async {
|
||||||
final data = context.read<DataService>();
|
final data = context.read<DataService>();
|
||||||
final filters = <String, dynamic>{};
|
final filters = <String, dynamic>{};
|
||||||
final name = _nameController.text.trim();
|
final name = _nameController.text.trim();
|
||||||
@@ -118,15 +146,49 @@ class _TractionPageState extends State<TractionPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
final hadOnly = !_hasFilters;
|
final hadOnly = !_hasFilters;
|
||||||
|
final signature = _tractionQuerySignature(filters, hadOnly);
|
||||||
|
final queryChanged =
|
||||||
|
_lastQuerySignature != null && signature != _lastQuerySignature;
|
||||||
|
_lastQuerySignature = signature;
|
||||||
|
|
||||||
|
if (queryChanged && !append) {
|
||||||
|
_lastTractionOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final shouldPreservePosition = preservePosition &&
|
||||||
|
!append &&
|
||||||
|
!queryChanged &&
|
||||||
|
_lastTractionOffset > 0;
|
||||||
|
|
||||||
|
int limit;
|
||||||
|
int offset;
|
||||||
|
if (append) {
|
||||||
|
offset = data.traction.length;
|
||||||
|
limit = _pageSize;
|
||||||
|
_lastTractionOffset = offset;
|
||||||
|
} else if (shouldPreservePosition) {
|
||||||
|
offset = 0;
|
||||||
|
limit = _pageSize + _lastTractionOffset;
|
||||||
|
} else {
|
||||||
|
offset = 0;
|
||||||
|
limit = _pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
await data.fetchTraction(
|
await data.fetchTraction(
|
||||||
hadOnly: hadOnly,
|
hadOnly: hadOnly,
|
||||||
locoClass: _selectedClass ?? _classController.text.trim(),
|
locoClass: _selectedClass ?? _classController.text.trim(),
|
||||||
locoNumber: _numberController.text.trim(),
|
locoNumber: _numberController.text.trim(),
|
||||||
offset: append ? data.traction.length : 0,
|
offset: offset,
|
||||||
|
limit: limit,
|
||||||
append: append,
|
append: append,
|
||||||
filters: filters,
|
filters: filters,
|
||||||
mileageFirst: _mileageFirst,
|
mileageFirst: _mileageFirst,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!append && !shouldPreservePosition) {
|
||||||
|
_lastTractionOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
await _persistSearchState();
|
await _persistSearchState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,16 @@ extension _TractionPersistence on _TractionPageState {
|
|||||||
enumValues[entry.key.toString()] = entry.value?.toString();
|
enumValues[entry.key.toString()] = entry.value?.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
final lastOffsetRaw = decoded['lastOffset'];
|
||||||
|
if (lastOffsetRaw is int) {
|
||||||
|
_lastTractionOffset = lastOffsetRaw;
|
||||||
|
} else if (lastOffsetRaw is num) {
|
||||||
|
_lastTractionOffset = lastOffsetRaw.toInt();
|
||||||
|
}
|
||||||
|
final lastSig = decoded['querySignature']?.toString();
|
||||||
|
if (lastSig != null && lastSig.isNotEmpty) {
|
||||||
|
_lastQuerySignature = lastSig;
|
||||||
|
}
|
||||||
|
|
||||||
for (final entry in dynamicValues.entries) {
|
for (final entry in dynamicValues.entries) {
|
||||||
_dynamicControllers.putIfAbsent(
|
_dynamicControllers.putIfAbsent(
|
||||||
@@ -76,6 +86,8 @@ extension _TractionPersistence on _TractionPageState {
|
|||||||
'showAdvancedFilters': _showAdvancedFilters,
|
'showAdvancedFilters': _showAdvancedFilters,
|
||||||
'dynamic': _dynamicControllers.map((k, v) => MapEntry(k, v.text)),
|
'dynamic': _dynamicControllers.map((k, v) => MapEntry(k, v.text)),
|
||||||
'enum': _enumSelections,
|
'enum': _enumSelections,
|
||||||
|
'lastOffset': _lastTractionOffset,
|
||||||
|
'querySignature': _lastQuerySignature,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ extension DataServiceTraction on DataService {
|
|||||||
Future<void> fetchTraction({
|
Future<void> fetchTraction({
|
||||||
bool hadOnly = false,
|
bool hadOnly = false,
|
||||||
int offset = 0,
|
int offset = 0,
|
||||||
int limit = 50,
|
int limit = 100,
|
||||||
String? locoClass,
|
String? locoClass,
|
||||||
String? locoNumber,
|
String? locoNumber,
|
||||||
bool mileageFirst = true,
|
bool mileageFirst = true,
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class DistanceFormatter {
|
|||||||
return NumberFormat(pattern);
|
return NumberFormat(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatMilesChains(double miles, {int decimals = 1}) {
|
String _formatMilesChains(double miles) {
|
||||||
final totalChains = miles * DistanceUnitService.chainsPerMile;
|
final totalChains = miles * DistanceUnitService.chainsPerMile;
|
||||||
var milesPart = totalChains ~/ DistanceUnitService.chainsPerMile;
|
var milesPart = totalChains ~/ DistanceUnitService.chainsPerMile;
|
||||||
final chainRemainder =
|
final chainRemainder =
|
||||||
|
|||||||
@@ -18,18 +18,19 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="Log and explore your Mileograph journeys.">
|
||||||
|
<meta name="theme-color" content="#0175C2">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<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-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="mileograph_flutter">
|
<meta name="apple-mobile-web-app-title" content="Mileograph">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>mileograph_flutter</title>
|
<title>Mileograph</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mileograph_flutter",
|
"name": "Mileograph",
|
||||||
"short_name": "mileograph_flutter",
|
"short_name": "Mileograph",
|
||||||
"start_url": ".",
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0175C2",
|
"background_color": "#0175C2",
|
||||||
"theme_color": "#0175C2",
|
"theme_color": "#0175C2",
|
||||||
"description": "A new Flutter project.",
|
"description": "Log and explore your Mileograph journeys.",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
"icons": [
|
"icons": [
|
||||||
|
|||||||
Reference in New Issue
Block a user