Compare commits

13 Commits

Author SHA1 Message Date
julian 22aa22ce76 ci: correct apk location
Build and deploy the latest docker container / deploy (push) Successful in 51s
Build and release the latest android apk / rolling-release-apk (push) Successful in 4m53s
Build and release a versioned android apk / release-apk (push) Successful in 4m31s
2026-06-12 18:24:37 +02:00
julian d7d0668e29 Switch gradle home to local to fix gradle build in ci
Build and deploy the latest docker container / deploy (push) Successful in 46s
Build and release the latest android apk / rolling-release-apk (push) Successful in 4m13s
2026-06-12 09:07:38 +02:00
julian 76edfc5721 Remove jenkinsfile
Build and deploy the latest docker container / deploy (push) Successful in 46s
Build and release the latest android apk / rolling-release-apk (push) Failing after 12s
2026-06-12 09:00:54 +02:00
julian 99a79d8df9 devenv: remove unneeded deploy-android code
Build and deploy the latest docker container / deploy (push) Successful in 53s
Build and release the latest android apk / rolling-release-apk (push) Failing after 11s
2026-06-12 09:00:27 +02:00
julian 3affa455d0 ci: rework workflows to add one for android 2026-06-12 09:00:10 +02:00
julian 961a8d3918 ci: use nixos runner
Build and Deploy / deploy (push) Successful in 38m41s
2026-06-11 21:22:29 +02:00
julian 761372fb03 Move deploy to deploy-docker.yml ci workflow 2026-06-11 20:40:02 +02:00
julian 3f35c670ca Add gitea ci workflow
Build and Deploy / deploy (push) Failing after 37m6s
2026-06-04 18:14:53 +02:00
julian a32bfd5108 Update devenv.lock 2026-06-04 18:14:45 +02:00
julian 9261ec341e Add sync status to app drawer 2026-02-06 20:43:26 +01:00
julian 7ec7b20e74 Update version 2026-02-06 20:19:32 +01:00
julian 829bf7512a Only call setFullScreen if that changes fullscreen state 2026-02-06 20:16:00 +01:00
julian 4fe8896f9e Also upload annotations if page was cleared 2026-02-06 20:11:54 +01:00
12 changed files with 210 additions and 201 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Build and deploy the latest docker container
on:
push:
branches:
- master
jobs:
deploy:
runs-on: nixos
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build docker
run: devenv shell build-docker
- name: Deploy docker
env:
REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_GITEA_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_GITEA_PASSWORD }}
run: devenv shell deploy-docker
@@ -0,0 +1,37 @@
name: Build and release the latest android apk
on:
push:
branches:
- master
jobs:
rolling-release-apk:
runs-on: nixos
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Build apk
run: devenv shell build-android
- name: Move latest Tag
run: |
git config user.name "gitea-actions"
git config user.email "gitea-actions@localhost"
# Forcefully creates/moves the 'latest' tag to the current commit
git tag -f latest
# Force-pushes the updated tag back to your Gitea repository
git push origin -f latest
- name: Update Gitea Release & Swap APK
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch'
with:
tag_name: latest # Explicitly target the tag we just moved
name: 'Latest Development Build'
body: 'This rolling release is automatically generated and always contains the newest code from the main branch.'
files: build/app/outputs/flutter-apk/*.apk
token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,23 @@
name: Build and release a versioned android apk
on:
push:
tags:
- 'v*'
jobs:
release-apk:
runs-on: nixos
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Build apk
run: devenv shell build-android
- name: Create Gitea Release & Upload APK
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with:
files: build/app/outputs/flutter-apk/*.apk
token: ${{ secrets.GITHUB_TOKEN }}
+1
View File
@@ -56,3 +56,4 @@ devenv.local.nix
.android
android/app/.cxx/
/.gradle/
/.gitea/workflows/.gradle/
Vendored
-93
View File
@@ -1,93 +0,0 @@
pipeline {
agent {
node {
label 'devenv'
}
}
triggers {
pollSCM('H/10 * * * *')
}
stages {
stage('Test devenv') {
steps {
sh 'devenv test'
}
}
stage('Flutter builds') {
steps {
sh 'devenv shell dart run pdfrx:remove_wasm_modules --revert' // Make sure WASM is put in output
sh 'devenv shell flutter build web --release'
sh 'devenv shell dart run pdfrx:remove_wasm_modules' // NOT idempotent!
sh 'devenv shell flutter build apk --release'
sh 'devenv shell dart run pdfrx:remove_wasm_modules --revert' // Reset again, just to be sure
}
}
stage('Parallel Deploy') {
parallel {
stage('Web') {
steps {
sh 'docker build -t harbor.julian-mutter.de/sheetless/sheetless-frontend .'
withDockerRegistry([credentialsId: 'harbor', url: 'https://harbor.julian-mutter.de']) {
sh 'docker push harbor.julian-mutter.de/sheetless/sheetless-frontend'
}
}
}
stage('Android') {
stages {
stage('Checkout F-Droid Repo') {
steps {
dir("fdroid-repo") {
git branch: 'master', url: "https://gitlab.julian-mutter.de/julian/fdroid-frajul.git", credentialsId: "gitea", poll: "false"
}
}
}
stage('Decrypt keys') {
steps {
dir("fdroid-repo") {
sh 'devenv shell ./decrypt.sh'
}
}
}
stage('Copy APK') {
steps {
sh "cp build/app/outputs/apk/release/app-release.apk fdroid-repo/fdroid/repo/sheetless.apk"
}
}
stage('Update Metadata') {
steps {
dir("fdroid-repo") {
sh 'devenv shell ./update-repo.sh'
}
}
}
stage('Push updates') {
steps {
dir("fdroid-repo") {
withCredentials([gitUsernamePassword(credentialsId: 'gitea')]) {
sh '''
git config --global user.email "jenkins@comumail.de"
git config --global user.name "Jenkins CI"
./push-updates.sh
'''
}
}
}
}
}
}
}
}
}
post {
always {
step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: emailextrecipients([developers(), requestor()])])
cleanWs()
}
}
}
+2 -62
View File
@@ -16,62 +16,6 @@
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1769939035,
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "a8ca480175326551d6c4121498316261cbb5b260",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1762808025,
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1770115704,
@@ -90,14 +34,10 @@
"root": {
"inputs": {
"devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": [
"git-hooks"
]
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
}
+23 -5
View File
@@ -1,5 +1,4 @@
{ pkgs, ... }:
{
{pkgs, ...}: {
android = {
enable = true;
emulator.enable = false;
@@ -46,10 +45,29 @@
};
scripts = {
deploy-docker.exec = ''
build-docker.exec = ''
dart run pdfrx:remove_wasm_modules --revert // Make sure WASM is put in output
flutter build web --release
docker build -t harbor.julian-mutter.de/sheetless/sheetless-frontend .
docker push harbor.julian-mutter.de/sheetless/sheetless-frontend
docker build -t gitlab.julian-mutter.de/julian/sheetless-frontend:latest .
'';
deploy-docker.exec = ''
if [ -n "''${REGISTRY_USERNAME:-}" ] && [ -n "''${REGISTRY_PASSWORD:-}" ]; then
docker login gitlab.julian-mutter.de --username "$REGISTRY_USERNAME" --password "$REGISTRY_PASSWORD"
fi
docker push gitlab.julian-mutter.de/julian/sheetless-frontend
'';
build-android.exec = ''
export GRADLE_USER_HOME="$(pwd)/.gradle"
dart run pdfrx:remove_wasm_modules --revert // Make sure WASM is put in output
dart run pdfrx:remove_wasm_modules // NOT idempotent!
flutter build apk --release
'';
};
}
+19 -4
View File
@@ -11,12 +11,16 @@ class SyncResult {
final bool isOnline;
final int changesSynced;
final int annotationsSynced;
final int changesUnsynced;
final int annotationsUnsynced;
SyncResult({
required this.sheets,
required this.isOnline,
this.changesSynced = 0,
this.annotationsSynced = 0,
required this.changesSynced,
required this.annotationsSynced,
required this.changesUnsynced,
required this.annotationsUnsynced,
});
}
@@ -35,8 +39,8 @@ class SyncService {
SyncService({
required ApiClient apiClient,
required StorageService storageService,
}) : _apiClient = apiClient,
_storageService = storageService;
}) : _apiClient = apiClient,
_storageService = storageService;
/// Performs a full sync operation.
///
@@ -80,6 +84,8 @@ class SyncService {
// 3. Upload pending annotations
annotationsSynced = await _uploadPendingAnnotations();
final remainingAnnotations = await _storageService
.readPendingAnnotationUploads();
// 4. Apply any remaining local changes (in case some failed to upload)
final changeQueue = await _storageService.readChangeQueue();
@@ -102,6 +108,8 @@ class SyncService {
isOnline: true,
changesSynced: changesSynced,
annotationsSynced: annotationsSynced,
changesUnsynced: changeQueue.length,
annotationsUnsynced: remainingAnnotations.length,
);
}
@@ -127,9 +135,16 @@ class SyncService {
}
}
final remainingAnnotations = await _storageService
.readPendingAnnotationUploads();
return SyncResult(
sheets: sheets,
isOnline: false,
changesSynced: 0,
annotationsSynced: 0,
changesUnsynced: changeQueue.length,
annotationsUnsynced: remainingAnnotations.length,
);
}
+11 -3
View File
@@ -48,7 +48,9 @@ class _HomePageState extends State<HomePage> with RouteAware {
super.initState();
// Exit fullscreen when entering home page
FullScreen.setFullScreen(false);
if (FullScreen.isFullScreen) {
FullScreen.setFullScreen(false);
}
// Subscribe to route changes
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -71,14 +73,19 @@ class _HomePageState extends State<HomePage> with RouteAware {
@override
void didPush() {
FullScreen.setFullScreen(false);
// Exit fullscreen when entering home page
if (FullScreen.isFullScreen) {
FullScreen.setFullScreen(false);
}
super.didPush();
}
@override
void didPopNext() {
// Exit fullscreen when returning to home page
FullScreen.setFullScreen(false);
if (FullScreen.isFullScreen) {
FullScreen.setFullScreen(false);
}
super.didPopNext();
}
@@ -212,6 +219,7 @@ class _HomePageState extends State<HomePage> with RouteAware {
onLogout: _handleLogout,
appName: _appName,
appVersion: _appVersion,
syncFuture: _syncFuture,
),
body: RefreshIndicator(onRefresh: _refreshSheets, child: _buildBody()),
);
+47 -9
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:sheetless/core/services/sync_service.dart';
/// Callback for shuffle state changes.
typedef ShuffleCallback = void Function(bool enabled);
@@ -12,12 +13,14 @@ class AppDrawer extends StatelessWidget {
final VoidCallback onLogout;
final String? appName;
final String? appVersion;
final Future<SyncResult> syncFuture;
const AppDrawer({
super.key,
required this.isShuffling,
required this.onShuffleChanged,
required this.onLogout,
required this.syncFuture,
this.appName,
this.appVersion,
});
@@ -32,7 +35,7 @@ class AppDrawer extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildActions(),
_buildAppInfo(),
Column(children: [_buildSyncStatus(), _buildAppInfo()]),
],
),
),
@@ -44,10 +47,7 @@ class AppDrawer extends StatelessWidget {
return Column(
children: [
ListTile(
leading: Icon(
Icons.shuffle,
color: isShuffling ? Colors.blue : null,
),
leading: Icon(Icons.shuffle, color: isShuffling ? Colors.blue : null),
title: const Text('Shuffle'),
onTap: () => onShuffleChanged(!isShuffling),
),
@@ -60,6 +60,47 @@ class AppDrawer extends StatelessWidget {
);
}
Widget _buildSyncStatus() {
return Center(
// padding: const EdgeInsets.all(5.0),
child: FutureBuilder<SyncResult>(
future: syncFuture,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Text(
"Error: ${snapshot.error.toString()}",
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
);
}
if (snapshot.hasData) {
final changes = snapshot.data!.changesUnsynced;
final annotations = snapshot.data!.annotationsUnsynced;
if (changes == 0 && annotations == 0) {
return Text(
"All synced!",
style: const TextStyle(color: Colors.black),
textAlign: TextAlign.center,
);
}
return Text(
"$changes changes and $annotations annotations unsynchronized!",
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
);
}
return const Center(child: CircularProgressIndicator());
},
),
);
}
Widget _buildAppInfo() {
final versionText = appName != null && appVersion != null
? '$appName v$appVersion'
@@ -67,10 +108,7 @@ class AppDrawer extends StatelessWidget {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
versionText,
style: const TextStyle(color: Colors.grey),
),
child: Text(versionText, style: const TextStyle(color: Colors.grey)),
);
}
}
@@ -65,7 +65,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
_rightDrawingController = DrawingController(maxHistorySteps: 50);
FullScreen.addListener(this);
FullScreen.setFullScreen(widget.config.fullscreen);
if (FullScreen.isFullScreen != widget.config.fullscreen) {
FullScreen.setFullScreen(widget.config.fullscreen);
}
_documentLoaded = _loadPdf();
}
@@ -161,14 +163,12 @@ class _SheetViewerPageState extends State<SheetViewerPage>
);
// Upload left page to server
if (leftHasContent) {
_syncService.uploadAnnotation(
sheetUuid: widget.sheet.uuid,
page: _currentPage,
annotationsJson: leftJson,
lastModified: now,
);
}
_syncService.uploadAnnotation(
sheetUuid: widget.sheet.uuid,
page: _currentPage,
annotationsJson: leftJson,
lastModified: now,
);
_leftDrawingController.markSaved();
}
@@ -188,14 +188,12 @@ class _SheetViewerPageState extends State<SheetViewerPage>
);
// Upload right page to server
if (rightHasContent) {
_syncService.uploadAnnotation(
sheetUuid: widget.sheet.uuid,
page: _currentPage + 1,
annotationsJson: rightJson,
lastModified: now,
);
}
_syncService.uploadAnnotation(
sheetUuid: widget.sheet.uuid,
page: _currentPage + 1,
annotationsJson: rightJson,
lastModified: now,
);
_rightDrawingController.markSaved();
}
@@ -214,7 +212,7 @@ class _SheetViewerPageState extends State<SheetViewerPage>
}
void _toggleFullscreen() {
FullScreen.setFullScreen(!widget.config.fullscreen);
FullScreen.setFullScreen(!FullScreen.isFullScreen);
}
// ---------------------------------------------------------------------------
@@ -300,8 +298,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
icon: Icon(
widget.config.fullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
),
tooltip:
widget.config.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
tooltip: widget.config.fullscreen
? 'Exit Fullscreen'
: 'Enter Fullscreen',
onPressed: _toggleFullscreen,
),
IconButton(
@@ -313,8 +312,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
icon: Icon(
widget.config.twoPageMode ? Icons.filter_1 : Icons.filter_2,
),
tooltip:
widget.config.twoPageMode ? 'Single Page Mode' : 'Two Page Mode',
tooltip: widget.config.twoPageMode
? 'Single Page Mode'
: 'Two Page Mode',
onPressed: _toggleTwoPageMode,
),
],
@@ -346,8 +346,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
currentPageNumber: _currentPage,
config: widget.config,
leftDrawingController: _leftDrawingController,
rightDrawingController:
widget.config.twoPageMode ? _rightDrawingController : null,
rightDrawingController: widget.config.twoPageMode
? _rightDrawingController
: null,
drawingEnabled: _isPaintMode,
);
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.1.2
version: 0.2.0
environment:
sdk: ^3.0.0