Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df7bb5e181 | ||
|
|
e8598a2e43 | ||
|
|
ea5e5592a8 | ||
|
|
d87eabfd2a | ||
|
|
6c9c650b68 | ||
|
|
5ec71f6ae8 | ||
|
|
2566371ce9 |
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Install the download tools
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends ca-certificates curl xz-utils
|
||||
apt-get install -y --no-install-recommends ca-certificates curl xz-utils jq
|
||||
|
||||
- name: Set up Java
|
||||
uses: https://github.com/actions/setup-java@v4
|
||||
@@ -74,24 +74,122 @@ jobs:
|
||||
- name: Run tests
|
||||
run: flutter test
|
||||
|
||||
# Android accepts an update only when it is signed with the same key as
|
||||
# the installed app, so every release has to carry one key that never
|
||||
# changes. The debug key cannot be used: the runner generates a fresh one
|
||||
# on every run, which would make each apk a different app to android and
|
||||
# break every update through Obtainium.
|
||||
#
|
||||
# `android/app/build.gradle.kts` picks the key up from
|
||||
# `android/key.properties`, which is written here from the repository
|
||||
# secrets (Settings -> Actions -> Secrets). See CLAUDE.md for the
|
||||
# `keytool` command that creates the keystore.
|
||||
- name: Write the release signing key
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
# The act-runner executes `run` with sh (dash), not bash, so this
|
||||
# script has to stay posix - `set -o pipefail` for example dies
|
||||
# with "Illegal option -o pipefail" before anything else runs.
|
||||
set -eu
|
||||
|
||||
# Failing here is on purpose: a silently debug signed apk would be
|
||||
# installable once and never updatable afterwards.
|
||||
if [ -z "${KEYSTORE_BASE64:-}" ]; then
|
||||
echo "::error::The secret ANDROID_KEYSTORE_BASE64 is empty. Add the keystore secrets (see CLAUDE.md) before publishing a release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "$KEYSTORE_BASE64" | base64 -d > android/app/release-keystore.jks
|
||||
|
||||
# storeFile is relative to `android/app`. The file is gitignored.
|
||||
cat > android/key.properties <<PROPERTIES
|
||||
storeFile=release-keystore.jks
|
||||
storePassword=$KEYSTORE_PASSWORD
|
||||
keyAlias=$KEY_ALIAS
|
||||
keyPassword=$KEY_PASSWORD
|
||||
PROPERTIES
|
||||
|
||||
# One apk for all abis. `--split-per-abi` would produce three smaller
|
||||
# ones instead, at the cost of having to pick the right one manually.
|
||||
#
|
||||
# The release build is signed with a debug key, because
|
||||
# `android/app/build.gradle.kts` has no release signing config yet. To
|
||||
# ship the apk, add a keystore as a secret, write it into
|
||||
# `android/key.properties` here and reference it from the gradle file.
|
||||
# The version in the pubspec (`1.0.0+1`) never moves, so it is replaced
|
||||
# here: the version name becomes the tag of the release and the version
|
||||
# code the run number, which only ever grows. Android refuses to install
|
||||
# an apk whose version code is lower than the installed one, so the code
|
||||
# must never go backwards.
|
||||
- name: Build the apk
|
||||
run: flutter build apk --release
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version="${{ github.ref_name }}"
|
||||
version="${version#v}"
|
||||
|
||||
# `--build-name` only accepts a dotted number, so an unusual tag
|
||||
# ("nightly") keeps the version of the pubspec instead of failing
|
||||
# the build.
|
||||
case "$version" in
|
||||
*[!0-9.]* | '' ) buildName='' ;;
|
||||
* ) buildName="$version" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$buildName" ]; then
|
||||
flutter build apk --release \
|
||||
--build-name "$buildName" \
|
||||
--build-number "${{ github.run_number }}"
|
||||
else
|
||||
echo "::warning::'${{ github.ref_name }}' is not a dotted version, keeping the version of the pubspec."
|
||||
flutter build apk --release --build-number "${{ github.run_number }}"
|
||||
fi
|
||||
|
||||
# Gradle always writes the same file name, so the release the apk belongs
|
||||
# to would not be visible after downloading it.
|
||||
- name: Name the apk after the release
|
||||
run: mv build/app/outputs/flutter-apk/app-release.apk ac_web-${{ github.ref_name }}.apk
|
||||
|
||||
# Artifacts of v4 need gitea 1.22 or newer, older instances need @v3.
|
||||
# Obtainium reads `<host>/api/v1/repos/<owner>/<repo>/releases` and
|
||||
# downloads the *assets* of a release, so the apk has to hang off the
|
||||
# release itself. A workflow artifact is invisible to it: it lives under
|
||||
# the run, is zipped and needs a login.
|
||||
- name: Attach the apk to the release
|
||||
env:
|
||||
# The token gitea creates for the run. If the instance hands out a
|
||||
# read only token, put a personal access token with repository write
|
||||
# access in a secret and use that instead.
|
||||
TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||||
asset="ac_web-${{ github.ref_name }}.apk"
|
||||
|
||||
# The release event carries the id; the lookup by tag is only there
|
||||
# for a manual re-run of the job.
|
||||
releaseId="${{ github.event.release.id }}"
|
||||
if [ -z "$releaseId" ] || [ "$releaseId" = "null" ]; then
|
||||
release=$(curl -fsSL -H "Authorization: token $TOKEN" \
|
||||
"$api/releases/tags/${{ github.ref_name }}")
|
||||
releaseId=$(printf '%s' "$release" | jq -r '.id')
|
||||
fi
|
||||
|
||||
curl -fsSL -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-F "attachment=@$asset" \
|
||||
"$api/releases/$releaseId/assets?name=$asset" > /dev/null
|
||||
|
||||
echo "Attached $asset to release $releaseId."
|
||||
|
||||
# @v3 on purpose: `upload-artifact@v4` bundles `@actions/artifact` v2,
|
||||
# which refuses to run as soon as `GITHUB_SERVER_URL` is not github.com
|
||||
# ("GHESNotSupportedError") -- and on this instance it is the gitea host.
|
||||
# The check happens in the action itself, before any request, so it
|
||||
# cannot be fixed on the gitea side. @v3 talks to the older artifact api
|
||||
# gitea implements and still runs on node20, which the act-runner allows.
|
||||
- name: Upload the apk
|
||||
uses: https://github.com/actions/upload-artifact@v4
|
||||
uses: https://github.com/actions/upload-artifact@v3
|
||||
with:
|
||||
name: ac_web-android-${{ github.ref_name }}
|
||||
# Artifact paths are relative to the repository, not to the
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
apt-get install -y --no-install-recommends \
|
||||
clang cmake ninja-build pkg-config \
|
||||
libgtk-3-dev liblzma-dev libstdc++-12-dev \
|
||||
curl unzip xz-utils
|
||||
curl unzip xz-utils jq
|
||||
|
||||
# `subosito/flutter-action` cannot be used: it caches the sdk through
|
||||
# `actions/cache@v5`, and the act-runner of this gitea instance only
|
||||
@@ -72,9 +72,48 @@ jobs:
|
||||
- name: Pack the bundle
|
||||
run: tar -czf ac_web-linux-x64-${{ github.ref_name }}.tar.gz -C build/linux/x64/release bundle
|
||||
|
||||
# Artifacts of v4 need gitea 1.22 or newer, older instances need @v3.
|
||||
# The tarball hangs off the release itself, next to the apk of the
|
||||
# android job, so it can be downloaded without a login and stays after
|
||||
# the artifacts of the run have been pruned.
|
||||
- name: Attach the bundle to the release
|
||||
env:
|
||||
# The token gitea creates for the run. If the instance hands out a
|
||||
# read only token, put a personal access token with repository write
|
||||
# access in a secret and use that instead.
|
||||
TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# The act-runner executes `run` with sh (dash), not bash, so this
|
||||
# script has to stay posix - `set -o pipefail` for example dies
|
||||
# with "Illegal option -o pipefail" before anything else runs.
|
||||
set -eu
|
||||
|
||||
api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||||
asset="ac_web-linux-x64-${{ github.ref_name }}.tar.gz"
|
||||
|
||||
# The release event carries the id; the lookup by tag is only there
|
||||
# for a manual re-run of the job.
|
||||
releaseId="${{ github.event.release.id }}"
|
||||
if [ -z "$releaseId" ] || [ "$releaseId" = "null" ]; then
|
||||
release=$(curl -fsSL -H "Authorization: token $TOKEN" \
|
||||
"$api/releases/tags/${{ github.ref_name }}")
|
||||
releaseId=$(printf '%s' "$release" | jq -r '.id')
|
||||
fi
|
||||
|
||||
curl -fsSL -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-F "attachment=@$asset" \
|
||||
"$api/releases/$releaseId/assets?name=$asset" > /dev/null
|
||||
|
||||
echo "Attached $asset to release $releaseId."
|
||||
|
||||
# @v3 on purpose: `upload-artifact@v4` bundles `@actions/artifact` v2,
|
||||
# which refuses to run as soon as `GITHUB_SERVER_URL` is not github.com
|
||||
# ("GHESNotSupportedError") -- and on this instance it is the gitea host.
|
||||
# The check happens in the action itself, before any request, so it
|
||||
# cannot be fixed on the gitea side. @v3 talks to the older artifact api
|
||||
# gitea implements and still runs on node20, which the act-runner allows.
|
||||
- name: Upload the bundle
|
||||
uses: https://github.com/actions/upload-artifact@v4
|
||||
uses: https://github.com/actions/upload-artifact@v3
|
||||
with:
|
||||
name: ac_web-linux-x64-${{ github.ref_name }}
|
||||
# Artifact paths are relative to the repository, not to the
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
uses: https://github.com/docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
username: LocPac
|
||||
# A personal access token, created in the repo settings as BUILDTOKEN.
|
||||
password: ${{ secrets.BUILDTOKEN }}
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ Widget tests run against a mocked backend: build a `MockClient` that answers by
|
||||
|
||||
| Workflow | Builds | Result |
|
||||
| --- | --- | --- |
|
||||
| `BuildLinux.yml` | `flutter build linux --release` | `ac_web-linux-x64-<tag>.tar.gz` (the whole bundle directory) as an artifact |
|
||||
| `BuildAndroid.yml` | `flutter build apk --release` | `ac_web-<tag>.apk` as an artifact |
|
||||
| `BuildLinux.yml` | `flutter build linux --release` | `ac_web-linux-x64-<tag>.tar.gz` (the whole bundle directory), attached to the release and uploaded as an artifact |
|
||||
| `BuildAndroid.yml` | `flutter build apk --release` | `ac_web-<tag>.apk`, attached to the release and uploaded as an artifact |
|
||||
| `PublishDocker.yml` | `ac_web/Dockerfile` | image pushed to the Gitea registry as `<tag>`, `latest` and the short sha |
|
||||
|
||||
Notes that are easy to get wrong:
|
||||
@@ -47,15 +47,32 @@ Notes that are easy to get wrong:
|
||||
- **The act-runner of the gitea instance only supports actions up to `node20`.** Anything newer fails with `The runs.using key in action.yml must be one of: [...], got node24`, which is why the Flutter sdk is unpacked from the official archive instead of through `subosito/flutter-action` (that one caches through `actions/cache@v5`). Check the `runs.using` of an action before adding it.
|
||||
- The Flutter version is pinned in three places (`FLUTTER_VERSION` in both build workflows, `ARG FLUTTER_VERSION` in `ac_web/Dockerfile`) and must be kept in sync.
|
||||
- Steps run with `working-directory: ac_web`, but `upload-artifact` paths are relative to the repository root.
|
||||
- **The act-runner executes `run:` with sh (dash), not bash.** A script with `set -o pipefail` fails immediately with `Illegal option -o pipefail`, so the scripts stay posix (`set -eu`, no `[[ ]]`, no arrays) and pipelines are split where the exit code of the *first* command matters.
|
||||
- **`actions/upload-artifact` must stay at `@v3`.** `@v4` bundles `@actions/artifact` v2, which aborts with `GHESNotSupportedError` whenever `GITHUB_SERVER_URL` is not `github.com` -- on this instance it is the gitea host, so the check fires before any request and cannot be worked around on the server side. `@v3` uses the older artifact api gitea implements.
|
||||
- The workflows are read from the tag the release points at, so they have to be committed **before** the release is created.
|
||||
- Artifacts and the built files carry the tag name (`github.ref_name`); the docker image is pushed as the tag, as `latest` and as the short sha.
|
||||
- The apk is signed with a debug key, because `android/app/build.gradle.kts` still has no release signing config.
|
||||
- **The built files are attached to the release, not only uploaded as artifacts.** Artifacts live under the workflow run, are zipped and need a login; a release asset is a plain download on the release page. The upload is a `curl` against `POST <server>/api/v1/repos/<owner>/<repo>/releases/<id>/assets?name=<file>` with `${{ github.token }}`, and the release id comes from the event (falling back to a lookup by tag for a manual re-run). Obtainium needs the apk exactly there, see below.
|
||||
- **The apk must keep one signing key forever.** Android identifies an app by its application id *and* its certificate, so an update signed with a different key is rejected (`INSTALL_FAILED_UPDATE_INCOMPATIBLE`) and the app has to be uninstalled first. The debug key is regenerated on every runner, so it can never be used for a published apk. `android/app/build.gradle.kts` reads the key from `android/key.properties` (gitignored) and falls back to the debug key when that file is missing, so `flutter run --release` still works locally. The workflow writes the file from four secrets and fails loudly when the first one is empty: `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`. Create the keystore once and keep a backup — losing it means every installed app has to be uninstalled before it can be updated again:
|
||||
|
||||
```bash
|
||||
keytool -genkeypair -v -keystore ac_web-release.p12 -storetype PKCS12 \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 -alias ac-web
|
||||
base64 -w0 ac_web-release.p12 # the value of ANDROID_KEYSTORE_BASE64
|
||||
```
|
||||
|
||||
- **The apk version comes from the release, not from the pubspec.** `version: 1.0.0+1` never moves, so the workflow passes `--build-name <tag without a leading v>` and `--build-number ${{ github.run_number }}`; the run number only grows, and android refuses an apk whose version code is lower than the installed one. A tag that is not a dotted number keeps the pubspec version and only warns.
|
||||
- **Obtainium** tracks the app through the Gitea api: add the repository url (`https://<host>/ArtificialCompany/AcWeb`) and pick `Codeberg (Forgejo)` under "Override Source" — self hosted instances are not detected automatically, but the source builds its api calls from the host in the url. It reads `/api/v1/repos/.../releases` and downloads the release assets, so the repository has to be readable without a login from the phone.
|
||||
- The docker image is self contained: it compiles the web bundle itself, so `docker build -f ac_web/Dockerfile ac_web` gives the same result locally. It serves the static bundle with nginx (`ac_web/nginx.conf`), which falls back to `index.html` so deep links survive a reload. The backend address is compiled into the bundle, so the host in `main.dart` must be reachable from the browser of every visitor, not only from the container.
|
||||
|
||||
## Backend connection
|
||||
|
||||
The backend host/port is configured once in `lib/main.dart` via `AcApi.configure(const ApiConfig(host: 'localhost', port: 5118))`. Plain http is intentional — the API is only reached inside a trusted network. There is no environment/config file; changing the backend address means editing `main.dart`.
|
||||
|
||||
On android two things in `android/app/src/main/AndroidManifest.xml` have to stay in place for that to work, and both only bite the **release** apk:
|
||||
|
||||
- `<uses-permission android:name="android.permission.INTERNET"/>` — flutter ships this permission only in `src/debug` and `src/profile`, which a release build does not merge. Without it every request fails with `SocketException: Connection failed (OS Error: Operation not permitted)`, because the app may not open a socket at all.
|
||||
- `android:usesCleartextTraffic="true"` on `<application>` — android blocks plain http since api 28 (the app targets 36). Without it requests fail with `CLEARTEXT communication to <host> not permitted`. Remove the attribute once the backend speaks https.
|
||||
|
||||
## Architecture
|
||||
|
||||
### API layer (`lib/Api/`)
|
||||
@@ -80,7 +97,7 @@ Enums (`EAgentType`, `EAgentStatus`, `EReportType`) mirror .NET enums. **The dec
|
||||
|
||||
- `MainApp` picks the layout by width: `>= DesktopView.minimumWidth` (900) → `DesktopView` (three columns side by side: Reports | LatestReports | Agents), otherwise `MobileView` (same three pages behind a `NavigationBar`). Both layouts reuse the *same* page widgets, so pages must not assume they own the whole screen.
|
||||
- **No state management package and no routing package.** Data loading is consistently: `StatefulWidget` → `late Future<T> mXFuture` assigned in `initState` from `AcApi.instance...` → `FutureBuilder` with explicit loading / `snapshot.hasError` / empty-list branches. Reload = reassign the future inside `setState`. Navigation is plain `Navigator.of(context).push(MaterialPageRoute(...))`.
|
||||
- **Automatic refresh**: every widget that shows api data polls the backend (it has no push channel) through the `AutoRefreshState` mixin (`lib/Widgets/AutoRefreshState.dart`): mix it into the `State` and implement `RefreshData`, which fetches again and, only on success, assigns the result as an already completed future. The mixin owns the timer, guards against overlapping requests and swallows errors of a tick. The interval (30 s) lives once in `AutoRefreshState.refreshInterval`. Because a refresh must not flash the spinner or drop the list, the `FutureBuilder`s branch on `snapshot.hasData` / `hasError`, **not** on `connectionState`. A refresh the user did not ask for stays silent — no snackbars, no disabled buttons (see `AgentDetails._reloadAgent`).
|
||||
- **Automatic refresh**: every widget that shows api data polls the backend (it has no push channel) through the `AutoRefreshState` mixin (`lib/Widgets/AutoRefreshState.dart`): mix it into the `State` and implement `RefreshData`, which fetches again and, only on success, assigns the result as an already completed future. The mixin owns the timer, guards against overlapping requests and swallows errors of a tick. The interval (15 s) lives once in `AutoRefreshState.refreshInterval`. Because a refresh must not flash the spinner or drop the list, the `FutureBuilder`s branch on `snapshot.hasData` / `hasError`, **not** on `connectionState`. A refresh the user did not ask for stays silent — no snackbars, no disabled buttons (see `AgentDetails._reloadAgent`).
|
||||
- Icons per enum value live in extensions (`AgentTypeIcons`, `ReportTypeIcons` — `type.icon`) so every page renders a type identically. Human labels come from `displayName` on the enum itself.
|
||||
- `MarkdownText` is a hand-written markdown renderer (no `flutter_markdown` dependency) covering the subset the reports use: headings, lists, block quotes, fenced code, rules, and inline bold/italic/strikethrough/code/links. Extend it rather than adding a package.
|
||||
- Dependencies are intentionally minimal (`http`, `cupertino_icons`). Prefer solving things with Flutter's own widgets before adding a package.
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// Android identifies an app by its application id *and* the certificate it was
|
||||
// signed with, so an update is only accepted when it carries the same
|
||||
// signature as the installed app. Obtainium therefore needs every release to
|
||||
// be signed with one key that never changes - the debug key does not work, it
|
||||
// is generated per machine and a fresh one appears on every ci run.
|
||||
//
|
||||
// The key itself is described by `android/key.properties` (storeFile,
|
||||
// storePassword, keyAlias, keyPassword). That file and the keystore are
|
||||
// gitignored and never committed: a developer creates them once locally, the
|
||||
// workflow writes them from the repository secrets. Without the file the
|
||||
// release build falls back to the debug key so that `flutter run --release`
|
||||
// keeps working on a machine that has no keystore.
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
val hasReleaseKeystore = keystorePropertiesFile.exists()
|
||||
val keystoreProperties = Properties()
|
||||
|
||||
if (hasReleaseKeystore) {
|
||||
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.fscheffers.ac_web"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -15,21 +37,37 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.fscheffers.ac_web"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
// Both come from the pubspec (`version: <name>+<code>`); the workflow
|
||||
// overrides them with the release tag and the run number.
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (hasReleaseKeystore) {
|
||||
create("release") {
|
||||
// A relative storeFile is resolved against `android/app`, an
|
||||
// absolute one is taken as it is.
|
||||
storeFile = file(keystoreProperties.getProperty("storeFile"))
|
||||
storePassword = keystoreProperties.getProperty("storePassword")
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = if (hasReleaseKeystore) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Only `src/debug` and `src/profile` carry this permission by default,
|
||||
because the flutter tool needs it for hot reload. A release build
|
||||
merges this manifest alone, so without the line here every request of
|
||||
the release apk fails with "Operation not permitted" (EPERM) - the app
|
||||
is not allowed to open a socket at all. -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:label="ac_web"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<!-- The api is reached over plain http inside a trusted network (see
|
||||
`AcApi.configure` in `lib/main.dart`), and android blocks cleartext
|
||||
traffic by default since api 28. Drop this attribute as soon as the
|
||||
backend is served over https. -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
class ApiConfig {
|
||||
const ApiConfig({
|
||||
this.scheme = 'http',
|
||||
this.host = 'localhost',
|
||||
this.port = 5118,
|
||||
this.host = '10.10.50.30',
|
||||
this.port = 12781,
|
||||
this.basePath = '',
|
||||
this.timeout = const Duration(seconds: 15),
|
||||
this.defaultHeaders = const {},
|
||||
|
||||
@@ -15,13 +15,13 @@ import 'package:flutter/material.dart';
|
||||
///
|
||||
/// A refresh happens quietly: implementations are expected to keep the data
|
||||
/// that is already on screen while the request runs, so that the user neither
|
||||
/// sees a spinner nor loses the scroll position every half minute. Failures of
|
||||
/// sees a spinner nor loses the scroll position every few seconds. Failures of
|
||||
/// a refresh are swallowed on purpose - the data on screen simply stays as it
|
||||
/// is and the next tick tries again.
|
||||
mixin AutoRefreshState<TWidget extends StatefulWidget> on State<TWidget> {
|
||||
|
||||
/// Interval in which the api data of the whole app is refreshed.
|
||||
static const Duration refreshInterval = Duration(seconds: 30);
|
||||
static const Duration refreshInterval = Duration(seconds: 15);
|
||||
|
||||
Timer? _refreshTimer;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ void main() {
|
||||
|
||||
// Rest API of the backend. Plain http is fine, we only talk inside of a
|
||||
// trusted network.
|
||||
AcApi.configure(const ApiConfig(host: 'localhost', port: 5118));
|
||||
AcApi.configure(const ApiConfig(host: '10.10.50.30', port: 12781));
|
||||
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user