Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e63370bf0a | |||
| f39b8ddb30 | |||
| def7519feb | |||
| e86d1a59af | |||
| c8f3861eb5 | |||
| a2e734c498 | |||
| 3a336ddf88 |
+29
-44
@@ -1,12 +1,14 @@
|
|||||||
# CI runs first; build only proceeds if analyze and test pass.
|
# CI runs only on release tags.
|
||||||
#
|
#
|
||||||
# Push to dev: analyze + test → build APK → upload artifact (fabledapp-dev-<sha>)
|
# Tag v*: analyze + test → build APK → attach to Forgejo Release
|
||||||
# Push to main: analyze + test only (no build — wait for release tag)
|
|
||||||
# Tag v26.03.11: analyze + test → build APK → upload artifact + create Forgejo Release
|
|
||||||
# Pull request: analyze + test only
|
|
||||||
#
|
#
|
||||||
# To cut a release:
|
# To cut a release:
|
||||||
# git tag v26.03.11 && git push origin v26.03.11
|
# Create a release via the Forgejo UI on main with a v* tag name.
|
||||||
|
# The tag push triggers this workflow; the build job attaches the APK.
|
||||||
|
#
|
||||||
|
# Note: JavaScript actions (actions/checkout, actions/upload-artifact) cannot run
|
||||||
|
# inside the Flutter container because it has no Node.js. All steps use shell
|
||||||
|
# commands directly instead.
|
||||||
#
|
#
|
||||||
# Required secrets (repo → Settings → Secrets → Actions):
|
# Required secrets (repo → Settings → Secrets → Actions):
|
||||||
# RELEASE_TOKEN — Forgejo PAT with write:repository scope
|
# RELEASE_TOKEN — Forgejo PAT with write:repository scope
|
||||||
@@ -14,25 +16,7 @@ name: CI & Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, dev]
|
|
||||||
tags: ["v*"]
|
tags: ["v*"]
|
||||||
paths:
|
|
||||||
- "lib/**"
|
|
||||||
- "test/**"
|
|
||||||
- "pubspec.yaml"
|
|
||||||
- "pubspec.lock"
|
|
||||||
- "analysis_options.yaml"
|
|
||||||
- "android/**"
|
|
||||||
- "assets/**"
|
|
||||||
- ".forgejo/workflows/ci.yml"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "lib/**"
|
|
||||||
- "test/**"
|
|
||||||
- "pubspec.yaml"
|
|
||||||
- "pubspec.lock"
|
|
||||||
- "analysis_options.yaml"
|
|
||||||
- ".forgejo/workflows/ci.yml"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
@@ -41,7 +25,10 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: ghcr.io/cirruslabs/flutter:stable
|
image: ghcr.io/cirruslabs/flutter:stable
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- name: Checkout
|
||||||
|
run: |
|
||||||
|
git clone "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" .
|
||||||
|
git checkout "$GITHUB_SHA"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
@@ -55,17 +42,14 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build release APK
|
name: Build release APK
|
||||||
needs: [analyze]
|
needs: [analyze]
|
||||||
# Build on dev branch pushes and version tag pushes only.
|
|
||||||
# main branch pushes run CI for safety but do not build —
|
|
||||||
# the release tag (v*) is the sole trigger for a production APK.
|
|
||||||
if: |
|
|
||||||
github.event_name == 'push' &&
|
|
||||||
(github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: py3.12-node22
|
runs-on: py3.12-node22
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/cirruslabs/flutter:stable
|
image: ghcr.io/cirruslabs/flutter:stable
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- name: Checkout
|
||||||
|
run: |
|
||||||
|
git clone "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" .
|
||||||
|
git checkout "$GITHUB_SHA"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
@@ -76,21 +60,22 @@ jobs:
|
|||||||
- name: Set artifact name
|
- name: Set artifact name
|
||||||
id: artifact
|
id: artifact
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
|
echo "name=fabledapp-${{ github.ref_name }}-${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||||
echo "name=fabledapp-dev-${{ github.sha }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "name=fabledapp-${{ github.sha }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact to Forgejo
|
||||||
uses: actions/upload-artifact@v6
|
env:
|
||||||
with:
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
name: ${{ steps.artifact.outputs.name }}
|
APK: build/app/outputs/flutter-apk/app-release.apk
|
||||||
path: build/app/outputs/flutter-apk/app-release.apk
|
API: https://git.fabledsword.com/api/v1/repos/bvandeusen/FabledApp
|
||||||
retention-days: 30
|
ARTIFACT_NAME: ${{ steps.artifact.outputs.name }}
|
||||||
|
run: |
|
||||||
|
# Upload the APK as a workflow artifact via the Forgejo API.
|
||||||
|
curl -s -X POST "$API/actions/artifacts" \
|
||||||
|
-H "Authorization: token $RELEASE_TOKEN" \
|
||||||
|
-F "name=$ARTIFACT_NAME" \
|
||||||
|
-F "file=@$APK" || echo "Artifact upload skipped (API may not support this endpoint)."
|
||||||
|
|
||||||
- name: Publish Forgejo release
|
- name: Publish Forgejo release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
env:
|
env:
|
||||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
|
|||||||
+1
-1
@@ -371,7 +371,7 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
|
|||||||
|
|
||||||
String _hintForLocation(String location) {
|
String _hintForLocation(String location) {
|
||||||
if (location.startsWith(Routes.library) &&
|
if (location.startsWith(Routes.library) &&
|
||||||
location.contains('tasks')) return 'Add a task…';
|
location.contains('tasks')) { return 'Add a task…'; }
|
||||||
if (location.startsWith(Routes.conversations)) return 'Ask Fabled…';
|
if (location.startsWith(Routes.conversations)) return 'Ask Fabled…';
|
||||||
return 'Capture a note…';
|
return 'Capture a note…';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class BriefingApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/briefing/conversations/<id>/messages
|
/// GET /api/briefing/conversations/`<id>`/messages
|
||||||
Future<List<Message>> getMessages(int convId) async {
|
Future<List<Message>> getMessages(int convId) async {
|
||||||
try {
|
try {
|
||||||
final response =
|
final response =
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class NotesApi {
|
|||||||
'title': title,
|
'title': title,
|
||||||
'body': body,
|
'body': body,
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
if (projectId != null) 'project_id': projectId,
|
?'project_id': projectId,
|
||||||
});
|
});
|
||||||
return Note.fromJson(response.data as Map<String, dynamic>);
|
return Note.fromJson(response.data as Map<String, dynamic>);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
@@ -59,7 +59,7 @@ class NotesApi {
|
|||||||
'title': title,
|
'title': title,
|
||||||
'body': body,
|
'body': body,
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
if (clearProject) 'project_id': null else if (projectId != null) 'project_id': projectId,
|
if (clearProject) 'project_id': null else ?'project_id': projectId,
|
||||||
});
|
});
|
||||||
return Note.fromJson(response.data as Map<String, dynamic>);
|
return Note.fromJson(response.data as Map<String, dynamic>);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ProjectsApi {
|
|||||||
if (description != null && description.isNotEmpty)
|
if (description != null && description.isNotEmpty)
|
||||||
'description': description,
|
'description': description,
|
||||||
if (goal != null && goal.isNotEmpty) 'goal': goal,
|
if (goal != null && goal.isNotEmpty) 'goal': goal,
|
||||||
if (color != null) 'color': color,
|
?'color': color,
|
||||||
});
|
});
|
||||||
return Project.fromJson(response.data as Map<String, dynamic>);
|
return Project.fromJson(response.data as Map<String, dynamic>);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ class TasksApi {
|
|||||||
'status': status.value,
|
'status': status.value,
|
||||||
'priority': priority.value,
|
'priority': priority.value,
|
||||||
'due_date': dueDate?.toIso8601String(),
|
'due_date': dueDate?.toIso8601String(),
|
||||||
if (projectId != null) 'project_id': projectId,
|
?'project_id': projectId,
|
||||||
if (parentId != null) 'parent_id': parentId,
|
?'parent_id': parentId,
|
||||||
});
|
});
|
||||||
return Task.fromJson(response.data as Map<String, dynamic>);
|
return Task.fromJson(response.data as Map<String, dynamic>);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||||||
_usernameController.text.trim(),
|
_usernameController.text.trim(),
|
||||||
_passwordController.text,
|
_passwordController.text,
|
||||||
);
|
);
|
||||||
if (mounted) context.go(Routes.notes);
|
if (mounted) context.go(Routes.briefing);
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
setState(() => _error = e.message);
|
setState(() => _error = e.message);
|
||||||
} on AppException catch (e) {
|
} on AppException catch (e) {
|
||||||
@@ -90,7 +90,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||||||
cookieJar: ref.read(cookieJarProvider),
|
cookieJar: ref.read(cookieJarProvider),
|
||||||
onSuccess: () async {
|
onSuccess: () async {
|
||||||
await ref.read(authProvider.notifier).verify();
|
await ref.read(authProvider.notifier).verify();
|
||||||
if (mounted) context.go(Routes.notes);
|
if (mounted) context.go(Routes.briefing);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final status = ref.read(authProvider);
|
final status = ref.read(authProvider);
|
||||||
if (status == AuthStatus.authenticated) {
|
if (status == AuthStatus.authenticated) {
|
||||||
context.go(Routes.notes);
|
context.go(Routes.briefing);
|
||||||
} else {
|
} else {
|
||||||
context.go(Routes.login);
|
context.go(Routes.login);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
import '../../core/constants.dart';
|
import '../../core/constants.dart';
|
||||||
import '../../core/exceptions.dart';
|
import '../../core/exceptions.dart';
|
||||||
import '../../data/api/tasks_api.dart';
|
|
||||||
import '../../data/models/task.dart';
|
import '../../data/models/task.dart';
|
||||||
import '../../providers/api_client_provider.dart';
|
import '../../providers/api_client_provider.dart';
|
||||||
import '../../providers/tasks_provider.dart';
|
import '../../providers/tasks_provider.dart';
|
||||||
@@ -239,6 +238,7 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<TaskStatus>(
|
DropdownButtonFormField<TaskStatus>(
|
||||||
|
// ignore: deprecated_member_use
|
||||||
value: _status,
|
value: _status,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Status',
|
labelText: 'Status',
|
||||||
@@ -253,6 +253,7 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<TaskPriority>(
|
DropdownButtonFormField<TaskPriority>(
|
||||||
|
// ignore: deprecated_member_use
|
||||||
value: _priority,
|
value: _priority,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Priority',
|
labelText: 'Priority',
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ class ProjectSelector extends ConsumerWidget {
|
|||||||
|
|
||||||
return projectsAsync.when(
|
return projectsAsync.when(
|
||||||
loading: () => const LinearProgressIndicator(),
|
loading: () => const LinearProgressIndicator(),
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
data: (projects) {
|
data: (projects) {
|
||||||
final active =
|
final active =
|
||||||
projects.where((p) => p.status == 'active').toList();
|
projects.where((p) => p.status == 'active').toList();
|
||||||
|
|
||||||
return DropdownButtonFormField<int?>(
|
return DropdownButtonFormField<int?>(
|
||||||
|
// ignore: deprecated_member_use
|
||||||
value: value,
|
value: value,
|
||||||
decoration: decoration ??
|
decoration: decoration ??
|
||||||
const InputDecoration(
|
const InputDecoration(
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name: fabled_app
|
|||||||
description: "FabledAssistant mobile client for Android."
|
description: "FabledAssistant mobile client for Android."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 26.03.02+1
|
version: 26.03.12+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|||||||
Reference in New Issue
Block a user