1 Commits

Author SHA1 Message Date
758686bd32 updated ci to new standart 2025-12-13 09:57:42 +01:00
140 changed files with 97 additions and 11058 deletions

View File

@@ -1,25 +1,17 @@
**/.dockerignore # Ignore the node_modules directory
**/.env node_modules
**/.git
**/.gitignore # Ignore the dist directory
**/.project www
**/.settings
**/.toolstarget # Ignore the .git directory
**/.vs .git
**/.vscode
**/.idea # Ignore the .gitignore file
**/*.*proj.user .gitignore
**/*.dbmdl
**/*.jfm # Ignore the .dockerignore file
**/azds.yaml .dockerignore
**/bin Dockerfile
**/charts
**/docker-compose* .vscode/*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

462
.gitignore vendored
View File

@@ -1,432 +1,70 @@
### Csharp template # Specifies intentionally untracked files to ignore when using Git
## Ignore Visual Studio temporary files, build results, and # http://git-scm.com/docs/gitignore
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~ *~
*.dbmdl *.sw[mnpcod]
*.dbproj.schemaview .tmp
*.jfm *.tmp
*.pfx *.tmp.*
*.publishsettings UserInterfaceState.xcuserstate
orleans.codegen.cs $RECYCLE.BIN/
# Including strong name files can present a security risk *.log
# (https://github.com/github/gitignore/pull/2483#issue-259490424) log.txt
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects /.sourcemaps
Generated_Code/ /.versions
/coverage
# Backup & report files from converting an old project file # Ionic
# to a newer Visual Studio version. Backup files are not needed, /.ionic
# because we have git ;-) /www
_UpgradeReport_Files/ /platforms
Backup*/ /plugins
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files # Compiled output
*.mdf /dist
*.ldf /tmp
*.ndf /out-tsc
/bazel-out
# Business Intelligence projects # Node
*.rdl.data /node_modules
*.bim.layout npm-debug.log
*.bim_*.settings yarn-error.log
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes # IDEs and editors
FakesAssemblies/ .idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-project
*.sublime-workspace
# GhostDoc plugin setting file # Visual Studio Code
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
*.code-workspace .history/*
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs # Miscellaneous
*.cab /.angular
*.msi /.angular/cache
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### Angular template
## Angular ##
# compiled output
dist/
tmp/
app/**/*.js
app/**/*.js.map
# dependencies
node_modules/
bower_components/
# IDEs and editors
.idea/
# misc
.sass-cache/ .sass-cache/
connect.lock/ /.nx
coverage/ /.nx/cache
libpeerconnection.log/ /connect.lock
npm-debug.log /coverage
/libpeerconnection.log
testem.log testem.log
typings/ /typings
.angular/
# e2e
e2e/*.js
e2e/*.map
# System Files
.DS_Store/
# System files
.DS_Store
Thumbs.db

View File

@@ -1,116 +1,62 @@
image: node:lts-alpine
stages: stages:
- install - install
- lint - lint
- build - build
- test
- publish - publish
install-mobile: install:
stage: install stage: install
image: node:lts-alpine
before_script:
- cd src/WorkTime.WebMobile
script: script:
- npm install --prefer-offline - npm install --prefer-offline
cache: cache:
key: key:
files: files:
- src/WorkTime.WebMobile/package.json - package.json
paths: paths:
- src/WorkTime.WebMobile/node_modules - node_modules
install-backend: lint:
stage: install
image: mcr.microsoft.com/dotnet/sdk:9.0
script:
- dotnet restore
lint-mobile:
stage: lint stage: lint
image: node:lts-alpine needs: ["install"]
needs: ["install-mobile"]
before_script:
- cd src/WorkTime.WebMobile
script: script:
- npm run lint - npm run lint
cache: cache:
key: key:
files: files:
- src/WorkTime.WebMobile/package.json - package.json
paths: paths:
- src/WorkTime.WebMobile/node_modules - node_modules
policy: pull policy: pull
build-mobile: build:
stage: build stage: build
image: node:lts-alpine needs: ["lint"]
needs: ["lint-mobile"]
before_script:
- cd src/WorkTime.WebMobile
script: script:
- npm run build - npm run build
artifacts: artifacts:
paths: paths:
- $CI_PROJECT_DIR/src/WorkTime.WebMobile/www - $CI_PROJECT_DIR/www
expire_in: 10 minutes expire_in: 10 minutes
cache: cache:
key: key:
files: files:
- src/WorkTime.WebMobile/package.json - package.json
paths: paths:
- src/WorkTime.WebMobile/node_modules - node_modules
policy: pull policy: pull
build-backend: publish:
stage: build
image: mcr.microsoft.com/dotnet/sdk:9.0
script:
- dotnet restore
- dotnet build --configuration Release --no-restore
artifacts:
paths:
- "**/bin/Release"
expire_in: 10 minutes
dependencies:
- install-backend
test-backend:
stage: test
image: mcr.microsoft.com/dotnet/sdk:9.0
script:
- dotnet test --verbosity normal
dependencies:
- build-backend
publish-mobile:
stage: publish stage: publish
needs: ["build-mobile"] needs: ["build"]
image: docker:latest tags:
services: - docker
- name: docker:dind
alias: docker
before_script:
- cd src/WorkTime.WebMobile
script: script:
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//') - export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
- docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin registry.leon-hoppe.de
- docker build -t registry.leon-hoppe.de/leon.hoppe/worktime/mobile:$VERSION -t registry.leon-hoppe.de/leon.hoppe/worktime/mobile:latest . - docker build -t registry.leon-hoppe.de/leon.hoppe/worktime:$VERSION -t registry.leon-hoppe.de/leon.hoppe/worktime:latest .
- docker push registry.leon-hoppe.de/leon.hoppe/worktime/mobile:$VERSION - docker push registry.leon-hoppe.de/leon.hoppe/worktime:$VERSION
- docker push registry.leon-hoppe.de/leon.hoppe/worktime/mobile:latest - docker push registry.leon-hoppe.de/leon.hoppe/worktime:latest
only:
- tags
publish-backend:
stage: publish
image: docker:latest
services:
- name: docker:dind
alias: docker
script:
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
- docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de
- docker build -t registry.leon-hoppe.de/leon.hoppe/worktime/api:$VERSION -t registry.leon-hoppe.de/leon.hoppe/worktime/api:latest -f src/WorkTime.Api/Dockerfile .
- docker push registry.leon-hoppe.de/leon.hoppe/worktime/api:$VERSION
- docker push registry.leon-hoppe.de/leon.hoppe/worktime/api:latest
only: only:
- tags - tags

13
.idea/.gitignore generated vendored
View File

@@ -1,13 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/.idea.WorkTime.iml
/modules.xml
/projectSettingsUpdater.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,13 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/contentModel.xml
/projectSettingsUpdater.xml
/.idea.WorkTime.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders>
<Path>src/WorkTime.Mobile</Path>
<Path>src/WorkTime.WebMobile</Path>
</attachedFolders>
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
<option name="applicationTheme" value="default" />
<option name="iconsTheme" value="default" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
<option name="customApplicationId" value="" />
</component>
</project>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

4
.idea/encodings.xml generated
View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

8
.idea/indexLayout.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

14
.idea/misc.xml generated
View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
<option name="applicationTheme" value="default" />
<option name="iconsTheme" value="default" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
<option name="customApplicationId" value="" />
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,50 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{25C5A6B2-A1F9-4244-9538-18E3FE76D382}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.Host", "src\WorkTime.Host\WorkTime.Host.csproj", "{6F5D4D47-1484-44EA-A5DD-D00AAD2F2F68}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.ServiceDefaults", "src\WorkTime.ServiceDefaults\WorkTime.ServiceDefaults.csproj", "{B66AA463-03D5-4814-B1D4-71663804248C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.Shared", "src\WorkTime.Shared\WorkTime.Shared.csproj", "{E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.Api", "src\WorkTime.Api\WorkTime.Api.csproj", "{CED653D6-A0B6-432B-9C36-FBB58EEA8229}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{8CB1F4C6-F95B-4935-81AA-751015E69FEC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.Mobile", "src\WorkTime.Mobile\WorkTime.Mobile.csproj", "{640DF179-F955-4497-B798-43ABE2AAABF6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{6F5D4D47-1484-44EA-A5DD-D00AAD2F2F68} = {25C5A6B2-A1F9-4244-9538-18E3FE76D382}
{B66AA463-03D5-4814-B1D4-71663804248C} = {25C5A6B2-A1F9-4244-9538-18E3FE76D382}
{CED653D6-A0B6-432B-9C36-FBB58EEA8229} = {25C5A6B2-A1F9-4244-9538-18E3FE76D382}
{640DF179-F955-4497-B798-43ABE2AAABF6} = {8CB1F4C6-F95B-4935-81AA-751015E69FEC}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6F5D4D47-1484-44EA-A5DD-D00AAD2F2F68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F5D4D47-1484-44EA-A5DD-D00AAD2F2F68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F5D4D47-1484-44EA-A5DD-D00AAD2F2F68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F5D4D47-1484-44EA-A5DD-D00AAD2F2F68}.Release|Any CPU.Build.0 = Release|Any CPU
{B66AA463-03D5-4814-B1D4-71663804248C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B66AA463-03D5-4814-B1D4-71663804248C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B66AA463-03D5-4814-B1D4-71663804248C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B66AA463-03D5-4814-B1D4-71663804248C}.Release|Any CPU.Build.0 = Release|Any CPU
{E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}.Release|Any CPU.Build.0 = Release|Any CPU
{CED653D6-A0B6-432B-9C36-FBB58EEA8229}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CED653D6-A0B6-432B-9C36-FBB58EEA8229}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CED653D6-A0B6-432B-9C36-FBB58EEA8229}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CED653D6-A0B6-432B-9C36-FBB58EEA8229}.Release|Any CPU.Build.0 = Release|Any CPU
{640DF179-F955-4497-B798-43ABE2AAABF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{640DF179-F955-4497-B798-43ABE2AAABF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{640DF179-F955-4497-B798-43ABE2AAABF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{640DF179-F955-4497-B798-43ABE2AAABF6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,60 +0,0 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using WorkTime.Shared.Services;
namespace WorkTime.Api;
public class AuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IAuthService authService)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder) {
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (! await authService.IsAuthenticated()) {
return AuthenticateResult.Fail("Invalid or missing Guid.");
}
var guid = await authService.GetCurrentUserId();
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, guid.ToString()) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
public static Task ConfigureOpenApi(OpenApiDocument document, OpenApiDocumentTransformerContext transformerContext, CancellationToken token) {
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>();
document.SecurityRequirements ??= new List<OpenApiSecurityRequirement>();
document.Components.SecuritySchemes.Add(nameof(AuthHandler), new OpenApiSecurityScheme {
Type = SecuritySchemeType.Http,
In = ParameterLocation.Header,
Name = IAuthService.HeaderName,
Scheme = "Bearer",
Description = "GUID Authorization header using a custom scheme.\nExample: \"Authorization: {GUID}\""
});
//TODO: only add security requirement to authorized endpoints
document.SecurityRequirements.Add(new() {
{
new() {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = nameof(AuthHandler)
}
}, []
}
});
return Task.CompletedTask;
}
}

View File

@@ -1,13 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace WorkTime.Api.Controllers;
[ApiController, Route("auth")]
public class AuthController : ControllerBase {
[HttpGet("register")]
public ActionResult<Guid> Register() {
return Ok(Guid.NewGuid().ToString());
}
}

View File

@@ -1,35 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WorkTime.Api.Logic;
using WorkTime.Shared.Models;
namespace WorkTime.Api.Controllers;
[ApiController, Route("entries"), Authorize]
public class EntryController(EntryLogic logic) : ControllerBase {
[HttpGet("{id:guid}")]
public async Task<IResult> GetAllEntries(Guid id) {
var result = await logic.GetAllEntries(id);
return result.MapResult();
}
[HttpGet("{id:guid}/{day:datetime}")]
public async Task<IResult> GetEntries(Guid id, DateTime day) {
var result = await logic.GetEntries(id, DateOnly.FromDateTime(day));
return result.MapResult();
}
[HttpPost]
public async Task<IResult> AddEntry(TimeEntry entry) {
var result = await logic.AddEntry(entry);
return result.MapResult();
}
[HttpDelete("{id:int}")]
public async Task<IResult> DeleteEntry(int id) {
var result = await logic.DeleteEntry(id);
return result.MapResult();
}
}

View File

@@ -1,10 +0,0 @@
using Microsoft.EntityFrameworkCore;
using WorkTime.Shared.Models;
namespace WorkTime.Api;
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options) {
public DbSet<TimeEntry> Entries { get; set; }
}

View File

@@ -1,23 +0,0 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["src/WorkTime.Api/WorkTime.Api.csproj", "src/WorkTime.Api/"]
RUN dotnet restore "src/WorkTime.Api/WorkTime.Api.csproj"
COPY . .
WORKDIR "/src/src/WorkTime.Api"
RUN dotnet build "WorkTime.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "WorkTime.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WorkTime.Api.dll"]

View File

@@ -1,57 +0,0 @@
using Microsoft.AspNetCore.Http.HttpResults;
using WorkTime.ServiceDefaults.Results;
using WorkTime.Shared.Models;
using WorkTime.Shared.Repositories;
using WorkTime.Shared.Services;
namespace WorkTime.Api.Logic;
public class EntryLogic(IEntryRepository entryRepository, IAuthService authService) {
public async Task<LogicResult<TimeEntry[]>> GetAllEntries(Guid owner) {
if (!await authService.IsAuthenticated())
return new(Results.Unauthorized());
if (await authService.GetCurrentUserId() != owner)
return new(Results.Unauthorized());
return await entryRepository.GetAllEntries(owner);
}
public async Task<LogicResult<TimeEntry[]>> GetEntries(Guid owner, DateOnly date) {
if (!await authService.IsAuthenticated())
return new(Results.Unauthorized());
if (await authService.GetCurrentUserId() != owner)
return new(Results.Unauthorized());
return await entryRepository.GetEntries(owner, date);
}
public async Task<LogicResult> AddEntry(TimeEntry entry) {
if (!await authService.IsAuthenticated())
return new(Results.Unauthorized());
if (await authService.GetCurrentUserId() != entry.Owner)
return new(Results.Unauthorized());
await entryRepository.AddEntry(entry);
return new();
}
public async Task<LogicResult> DeleteEntry(int id) {
if (!await authService.IsAuthenticated())
return new(Results.Unauthorized());
var entry = await entryRepository.GetEntry(id);
if (entry is null)
return new(Results.NotFound());
if (await authService.GetCurrentUserId() != entry.Owner)
return new(Results.Unauthorized());
await entryRepository.DeleteEntry(entry);
return new();
}
}

View File

@@ -1,56 +0,0 @@
using MartinCostello.OpenApi;
using Microsoft.AspNetCore.Authentication;
using WorkTime.Api;
using WorkTime.Api.Logic;
using WorkTime.Api.Repositories;
using WorkTime.Api.Services;
using WorkTime.Shared.Repositories;
using WorkTime.Shared.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi(options => {
options.AddDocumentTransformer(AuthHandler.ConfigureOpenApi);
});
builder.Services.AddOpenApiExtensions(options => {
options.AddServerUrls = true;
});
builder.AddServiceDefaults();
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddHttpContextAccessor();
builder.Services.AddNpgsql<DatabaseContext>(builder.Configuration.GetConnectionString("data"));
builder.Services.AddScoped<IEntryRepository, EntryRepository>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<EntryLogic>();
builder.Services.AddAuthentication(nameof(AuthHandler))
.AddScheme<AuthenticationSchemeOptions, AuthHandler>(nameof(AuthHandler), _ => { });
builder.Services.AddAuthorization();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
app.MapOpenApi();
app.UseSwaggerUI(options => {
options.SwaggerEndpoint("/openapi/v1.json", "WorkTime.Api");
});
await using var scope = app.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
db.Database.EnsureCreated();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5212",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7091;http://localhost:5212",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,39 +0,0 @@
using Microsoft.EntityFrameworkCore;
using WorkTime.Shared.Models;
using WorkTime.Shared.Repositories;
namespace WorkTime.Api.Repositories;
internal class EntryRepository(DatabaseContext context) : IEntryRepository {
public async Task<TimeEntry[]> GetAllEntries(Guid owner) {
return await context.Entries
.Where(entry => entry.Owner == owner)
.OrderBy(entry => entry.Timestamp)
.ToArrayAsync();
}
public async Task<TimeEntry[]> GetEntries(Guid owner, DateOnly date) {
return await context.Entries
.Where(entry => entry.Owner == owner)
.Where(entry => DateOnly.FromDateTime(entry.Timestamp) == date)
.OrderBy(entry => entry.Timestamp)
.ToArrayAsync();
}
public async Task<TimeEntry?> GetEntry(int id) {
return await context.Entries
.FindAsync(id);
}
public async Task AddEntry(TimeEntry entry) {
await context.Entries.AddAsync(entry);
await context.SaveChangesAsync();
}
public async Task DeleteEntry(TimeEntry entry) {
context.Entries.Remove(entry);
await context.SaveChangesAsync();
}
}

View File

@@ -1,29 +0,0 @@
using WorkTime.Shared.Services;
namespace WorkTime.Api.Services;
internal class AuthService(IHttpContextAccessor accessor) : IAuthService {
public Task<bool> IsAuthenticated() {
var header = accessor.HttpContext?.Request.Headers[IAuthService.HeaderName];
if (header is not { Count: 1 })
return Task.FromResult(false);
var value = header.Value[0]!.Replace("Bearer ", "");
if (Guid.TryParse(value, out var guid))
return Task.FromResult(false);
return Task.FromResult(guid != Guid.Empty);
}
public async Task<Guid> GetCurrentUserId() {
if (!await IsAuthenticated())
return Guid.Empty;
var header = accessor.HttpContext?.Request.Headers[IAuthService.HeaderName]!;
var value = header.Value[0]!.Replace("Bearer ", "");
return Guid.Parse(value);
}
}

View File

@@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.1.0" />
<PackageReference Include="MartinCostello.OpenApi.Extensions" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.3.1" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WorkTime.ServiceDefaults\WorkTime.ServiceDefaults.csproj" />
<ProjectReference Include="..\WorkTime.Shared\WorkTime.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -1,10 +0,0 @@
var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddPostgres("db")
.WithDataVolume();
builder.AddProject<Projects.WorkTime_Api>("api")
.WithReference(db.AddDatabase("data"))
.WaitFor(db);
builder.Build().Run();

View File

@@ -1,29 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:17121;http://localhost:15199",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21125",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22099"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:15199",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19218",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20104"
}
}
}
}

View File

@@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0"/>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>ba72af6f-0952-417d-bef6-ab77ed6fa624</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.1.0" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WorkTime.Api\WorkTime.Api.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}

View File

@@ -1,14 +0,0 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:WorkTime.Mobile"
x:Class="WorkTime.Mobile.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,14 +0,0 @@
using MauiIcons.Core;
namespace WorkTime.Mobile;
public partial class App : Application {
public App() {
InitializeComponent();
_ = new MauiIcon();
}
protected override Window CreateWindow(IActivationState? activationState) {
return new Window(new AppShell());
}
}

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="WorkTime.Mobile.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:WorkTime.Mobile.Views.Pages"
xmlns:icons="http://www.aathifmahir.com/dotnet/2022/maui/icons"
FlyoutBehavior="Disabled"
Title="Zeiterfassung">
<Shell.Resources>
<ResourceDictionary>
<FontImageSource x:Key="CaptureIcon" Glyph="{icons:Material Schedule}" />
</ResourceDictionary>
</Shell.Resources>
<TabBar>
<ShellContent
Title="Erfassen"
Icon="{StaticResource CaptureIcon}"
ContentTemplate="{DataTemplate pages:CapturePage}"/>
</TabBar>
</Shell>

View File

@@ -1,7 +0,0 @@
namespace WorkTime.Mobile;
public partial class AppShell : Shell {
public AppShell() {
InitializeComponent();
}
}

View File

@@ -1,10 +0,0 @@
using Microsoft.EntityFrameworkCore;
using WorkTime.Shared.Models;
namespace WorkTime.Mobile;
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options) {
public DbSet<TimeEntry> Entries { get; set; }
}

View File

@@ -1,17 +0,0 @@
using WorkTime.Shared.Models;
namespace WorkTime.Mobile;
public static class Extensions {
public static string GetActionName(this TimeEntryType type) {
return type switch {
TimeEntryType.Login => "Einstempeln",
TimeEntryType.Logout => "Ausstempeln",
TimeEntryType.LoginDrive => "Dienstreise starten",
TimeEntryType.LogoutDrive => "Dienstreise beenden",
_ => string.Empty
};
}
}

View File

@@ -1,55 +0,0 @@
using MauiIcons.Material;
using Microsoft.Extensions.Logging;
using WorkTime.Mobile.Repositories;
using WorkTime.Mobile.Services;
using WorkTime.Mobile.ViewModels;
using WorkTime.Shared.Repositories;
using WorkTime.Shared.Services;
namespace WorkTime.Mobile;
public static class MauiProgram {
private static string BackendUrl { get; set; } = string.Empty; //TODO: Set production endpoint
public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.UseMaterialMauiIcons();
builder.Services.AddSqlite<DatabaseContext>($"Filename={Path.Combine(FileSystem.AppDataDirectory, "data.db")}");
builder.Services.AddSingleton(SecureStorage.Default);
builder.Services.AddScoped<HttpClient>(_ => {
var client = new HttpClient();
client.BaseAddress = new Uri(BackendUrl);
return client;
});
builder.Services.AddKeyedScoped<IEntryRepository, ServerEntryRepository>("server");
builder.Services.AddKeyedScoped<IEntryRepository, ClientEntryRepository>("client");
builder.Services.AddKeyedScoped<IHttpService, InsecureHttpService>("insecure");
builder.Services.AddScoped<IHttpService, HttpService>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<EntryService>();
builder.Services.AddTransient<CaptureViewModel>();
#if DEBUG
builder.Logging.AddDebug();
BackendUrl = "https://localhost:7091/";
#endif
var app = builder.Build();
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
db.Database.EnsureCreated();
return app;
}
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -1,10 +0,0 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace WorkTime.Mobile;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity { }

View File

@@ -1,12 +0,0 @@
using Android.App;
using Android.Runtime;
namespace WorkTime.Mobile;
[Application]
public class MainApplication : MauiApplication {
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership) { }
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

View File

@@ -1,8 +0,0 @@
using Foundation;
namespace WorkTime.Mobile;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate {
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
<dict>
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -1,13 +0,0 @@
using ObjCRuntime;
using UIKit;
namespace WorkTime.Mobile;
public class Program {
// This is the main entry point of the application.
static void Main(string[] args) {
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -1,14 +0,0 @@
using System;
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
namespace WorkTime.Mobile;
class Program : MauiApplication {
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
static void Main(string[] args) {
var app = new Program();
app.Run(args);
}
}

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="9" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="WorkTime.Mobile.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
</manifest>

View File

@@ -1,8 +0,0 @@
<maui:MauiWinUIApplication
x:Class="WorkTime.Mobile.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:WorkTime.Mobile.WinUI">
</maui:MauiWinUIApplication>

View File

@@ -1,21 +0,0 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace WorkTime.Mobile.WinUI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication {
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App() {
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<mp:PhoneIdentity PhoneProductId="1D7A4BF8-7BA9-4017-B89B-DBC4682E080F" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="WorkTime.Mobile.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -1,8 +0,0 @@
using Foundation;
namespace WorkTime.Mobile;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate {
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -1,13 +0,0 @@
using ObjCRuntime;
using UIKit;
namespace WorkTime.Mobile;
public class Program {
// This is the main entry point of the application.
static void Main(string[] args) {
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.
You are responsible for adding extra entries as needed for your application.
More information: https://aka.ms/maui-privacy-manifest
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<!--
The entry below is only needed when you're using the Preferences API in your app.
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict> -->
</array>
</dict>
</plist>

View File

@@ -1,8 +0,0 @@
{
"profiles": {
"Windows Machine": {
"commandName": "Project",
"nativeDebugging": false
}
}
}

View File

@@ -1,49 +0,0 @@
using Microsoft.EntityFrameworkCore;
using WorkTime.Shared.Models;
using WorkTime.Shared.Repositories;
namespace WorkTime.Mobile.Repositories;
public class ClientEntryRepository(DatabaseContext context) : IEntryRepository {
public async Task<TimeEntry[]> GetAllEntries(Guid owner) {
return await context.Entries
.Where(entry => entry.Owner == owner)
.OrderBy(entry => entry.Timestamp)
.ToArrayAsync();
}
public async Task<TimeEntry[]> GetEntries(Guid owner, DateOnly date) {
return await context.Entries
.Where(entry => entry.Owner == owner)
.Where(entry => DateOnly.FromDateTime(entry.Timestamp) == date)
.OrderBy(entry => entry.Timestamp)
.ToArrayAsync();
}
public async Task<TimeEntry?> GetEntry(int id) {
return await context.Entries
.FindAsync(id);
}
public async Task AddEntry(TimeEntry entry) {
await context.Entries.AddAsync(entry);
await context.SaveChangesAsync();
}
public async Task DeleteEntry(TimeEntry entry) {
context.Entries.Remove(entry);
await context.SaveChangesAsync();
}
public async Task ReplaceEntries(IEnumerable<TimeEntry> entries, DateOnly date) {
var oldEntries = await context.Entries
.Where(entry => DateOnly.FromDateTime(entry.Timestamp) == date)
.ToArrayAsync();
context.Entries.RemoveRange(oldEntries);
await context.Entries.AddRangeAsync(entries);
await context.SaveChangesAsync();
}
}

View File

@@ -1,29 +0,0 @@
using WorkTime.Mobile.Services;
using WorkTime.Shared.Models;
using WorkTime.Shared.Repositories;
namespace WorkTime.Mobile.Repositories;
public class ServerEntryRepository(IHttpService client) : IEntryRepository {
public async Task<TimeEntry[]> GetAllEntries(Guid owner) {
var response = await client.SendRequest<TimeEntry[]>(HttpMethod.Get, $"entries/{owner}");
return response.Result ?? [];
}
public async Task<TimeEntry[]> GetEntries(Guid owner, DateOnly date) {
var response = await client.SendRequest<TimeEntry[]>(HttpMethod.Get, $"entries/{owner}/{date.ToDateTime(TimeOnly.MinValue)}");
return response.Result ?? [];
}
public Task<TimeEntry?> GetEntry(int id) => Task.FromResult<TimeEntry?>(null);
public async Task AddEntry(TimeEntry entry) {
await client.SendRequest(HttpMethod.Post, "entries", entry);
}
public async Task DeleteEntry(TimeEntry entry) {
await client.SendRequest(HttpMethod.Delete, $"entries/{entry.Id}");
}
}

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

Before

Width:  |  Height:  |  Size: 228 B

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -1,15 +0,0 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
These files will be deployed with your package and will be accessible using Essentials:
async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="PrimaryDark">#ac99ea</Color>
<Color x:Key="PrimaryDarkText">#242424</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="SecondaryDarkText">#9880e5</Color>
<Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Magenta">#D600AA</Color>
<Color x:Key="MidnightBlue">#190649</Color>
<Color x:Key="OffBlack">#1f1f1f</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
</ResourceDictionary>

View File

@@ -1,451 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
</Style>
<Style TargetType="BoxView">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Span">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="Label" x:Key="Headline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="32" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Label" x:Key="SubHeadline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!--
<Style TargetType="TitleBar">
<Setter Property="MinimumHeightRequest" Value="32"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="TitleActiveStates">
<VisualState x:Name="TitleBarTitleActive">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="TitleBarTitleInactive">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
-->
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>

View File

@@ -1,23 +0,0 @@
using WorkTime.Shared.Services;
namespace WorkTime.Mobile.Services;
public class AuthService(ISecureStorage storage, [FromKeyedServices("insecure")] IHttpService httpService) : IAuthService {
public async Task<bool> IsAuthenticated() {
var value = await storage.GetAsync(IAuthService.HeaderName);
return !string.IsNullOrWhiteSpace(value);
}
public async Task<Guid> GetCurrentUserId() {
var value = await storage.GetAsync(IAuthService.HeaderName);
if (string.IsNullOrWhiteSpace(value)) {
var response = await httpService.SendRequest<string>(HttpMethod.Get, "auth/register");
value = response.Result ?? Guid.NewGuid().ToString();
}
return Guid.Parse(value);
}
}

View File

@@ -1,66 +0,0 @@
using WorkTime.Mobile.Repositories;
using WorkTime.Shared.Models;
using WorkTime.Shared.Repositories;
using WorkTime.Shared.Services;
namespace WorkTime.Mobile.Services;
public class EntryService(
[FromKeyedServices("server")] IEntryRepository serverRepository,
[FromKeyedServices("client")] IEntryRepository clientRepository,
IAuthService authService) {
public async IAsyncEnumerable<UpdatingEntriesResponse> GetEntries(DateOnly date) {
var userId = await authService.GetCurrentUserId();
var clientEntries = await clientRepository.GetEntries(userId, date);
yield return new() {
NewBatch = true
};
foreach (var entry in clientEntries) {
yield return new() {
Entry = entry
};
}
var serverEntries = await serverRepository.GetEntries(userId, date);
if (serverEntries.Length == 0) yield break;
yield return new() {
NewBatch = true
};
foreach (var entry in serverEntries) {
yield return new() {
Entry = entry
};
}
await ((ClientEntryRepository)clientRepository).ReplaceEntries(serverEntries, date);
}
public async Task AddEntry(TimeEntry entry) {
var userId = await authService.GetCurrentUserId();
entry.Owner = userId;
await Task.WhenAll(
clientRepository.AddEntry(entry),
serverRepository.AddEntry(entry)
);
}
public async Task DeleteEntry(TimeEntry entry) {
await Task.WhenAll(
clientRepository.DeleteEntry(entry),
serverRepository.DeleteEntry(entry)
);
}
}
public readonly struct UpdatingEntriesResponse {
public bool NewBatch { get; init; }
public TimeEntry? Entry { get; init; }
}

View File

@@ -1,101 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Text;
using System.Text.Json;
using WorkTime.Shared.Services;
namespace WorkTime.Mobile.Services;
public interface IHttpService {
Task<HttpResponse<TResult>> SendRequest<TResult>(HttpMethod method, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, object? body = null);
Task<HttpResponse> SendRequest(HttpMethod method, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, object? body = null);
}
internal class HttpService(HttpClient client, IAuthService authService) : IHttpService {
private InsecureHttpService? _service;
public async Task<HttpResponse<TResult>> SendRequest<TResult>(HttpMethod method, string uri, object? body = null) {
var service = await GetInternalService();
return await service.SendRequest<TResult>(method, uri, body);
}
public async Task<HttpResponse> SendRequest(HttpMethod method, string uri, object? body = null) {
var service = await GetInternalService();
return await service.SendRequest(method, uri, body);
}
private async Task<IHttpService> GetInternalService() {
if (_service is null) {
var id = await authService.GetCurrentUserId();
client.DefaultRequestHeaders.Add(IAuthService.HeaderName, id.ToString());
_service = new InsecureHttpService(client);
}
return _service;
}
}
internal class InsecureHttpService(HttpClient client) : IHttpService {
public async Task<HttpResponse<TResult>> SendRequest<TResult>(HttpMethod method, string uri, object? body = null) {
try {
var request = new HttpRequestMessage(method, uri);
if (body is not null)
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var responseContent = await response.Content.ReadAsStringAsync();
return new HttpResponse<TResult> {
Result = typeof(TResult) == typeof(string) ? (TResult)(object)responseContent : JsonSerializer.Deserialize<TResult>(responseContent),
ResponseCode = response.StatusCode,
IsSuccessful = response.IsSuccessStatusCode
};
}
catch (Exception) {
return new() {
IsSuccessful = false,
ResponseCode = HttpStatusCode.InternalServerError
};
}
}
public async Task<HttpResponse> SendRequest(HttpMethod method, string uri, object? body = null) {
try {
var request = new HttpRequestMessage(method, uri);
if (body is not null)
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
return new HttpResponse {
ResponseCode = response.StatusCode,
IsSuccessful = response.IsSuccessStatusCode
};
}
catch (Exception) {
return new() {
IsSuccessful = false,
ResponseCode = HttpStatusCode.InternalServerError
};
}
}
}
public readonly struct HttpResponse<TResult> {
public TResult? Result { get; init; }
public HttpStatusCode ResponseCode { get; init; }
public bool IsSuccessful { get; init; }
public static implicit operator TResult?(HttpResponse<TResult> response) {
return response.Result;
}
}
public readonly struct HttpResponse {
public HttpStatusCode ResponseCode { get; init; }
public bool IsSuccessful { get; init; }
}

View File

@@ -1,77 +0,0 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using WorkTime.Mobile.Services;
using WorkTime.Shared.Models;
using WorkTime.Shared.Services;
namespace WorkTime.Mobile.ViewModels;
public partial class CaptureViewModel(EntryService entryService, IAuthService authService) : ObservableObject {
[ObservableProperty]
public partial ObservableCollection<TimeEntry> Entries { get; set; } = new();
[ObservableProperty]
public partial TimeEntryType CurrentAction { get; set; } = TimeEntryType.Login;
[ObservableProperty]
public partial bool CurrentlyMoba { get; set; }
public string CurrentActionName => CurrentAction.GetActionName();
[RelayCommand]
private async Task OnDateSelected(DateOnly date) {
await foreach (var entryResponse in entryService.GetEntries(date)) {
if (entryResponse.NewBatch)
Entries.Clear();
if (entryResponse.Entry is not null)
Entries.Add(entryResponse.Entry);
}
UpdateCurrentAction();
}
[RelayCommand]
private async Task RegisterEntry(TimeEntry? entry = null) {
entry ??= new TimeEntry {
Type = CurrentAction,
IsMoba = CurrentlyMoba,
Owner = await authService.GetCurrentUserId()
};
var insertIndex = Entries.Index()
.LastOrDefault(e => e.Item.Timestamp < entry.Timestamp)
.Index;
if (Entries.Count == insertIndex + 1 || Entries.Count == 0)
Entries.Add(entry);
else Entries.Insert(insertIndex + 1, entry);
await entryService.AddEntry(entry);
UpdateCurrentAction();
}
private void UpdateCurrentAction() {
if (Entries.Count == 0) {
CurrentAction = TimeEntryType.Login;
CurrentlyMoba = false;
return;
}
var lastEntry = Entries[^1];
CurrentAction = lastEntry.Type switch {
TimeEntryType.Login => TimeEntryType.Logout,
TimeEntryType.Logout => TimeEntryType.Login,
TimeEntryType.LoginDrive => TimeEntryType.LogoutDrive,
TimeEntryType.LogoutDrive => TimeEntryType.Login,
_ => TimeEntryType.Login
};
CurrentlyMoba = lastEntry is { Type: TimeEntryType.Login, IsMoba: true };
}
partial void OnCurrentActionChanged(TimeEntryType value) {
OnPropertyChanged(nameof(CurrentActionName));
}
}

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:components="clr-namespace:WorkTime.Mobile.Views.Components"
x:Class="WorkTime.Mobile.Views.Components.DateSelector"
x:DataType="components:DateSelector"
x:Name="Component">
<Grid ColumnDefinitions="*,Auto">
<Label
Grid.Column="0"
Text="Tag"
VerticalOptions="Center"/>
<DatePicker
Grid.Column="1"
Date="{Binding CurrentDate, Source={x:Reference Component}}"
MaximumDate="{Binding MaxDate, Source={x:Reference Component}}"
DateSelected="OnDateSelected"/>
</Grid>
</ContentView>

View File

@@ -1,47 +0,0 @@
using CommunityToolkit.Mvvm.Input;
namespace WorkTime.Mobile.Views.Components;
public partial class DateSelector : ContentView {
public static readonly BindableProperty CurrentDateProperty = BindableProperty.Create(
nameof(CurrentDate),
typeof(DateTime),
typeof(DateSelector),
DateTime.Now);
public static readonly BindableProperty MaxDateProperty = BindableProperty.Create(
nameof(MaxDate),
typeof(DateTime),
typeof(DateSelector),
DateTime.Now);
public static readonly BindableProperty CommandProperty = BindableProperty.Create(
nameof(Command),
typeof(IRelayCommand<DateOnly>),
typeof(DateSelector));
public DateTime CurrentDate {
get => (DateTime)GetValue(CurrentDateProperty);
set => SetValue(CurrentDateProperty, value);
}
public DateTime MaxDate {
get => (DateTime)GetValue(MaxDateProperty);
set => SetValue(MaxDateProperty, value);
}
public IRelayCommand<DateOnly>? Command {
get => (IRelayCommand<DateOnly>)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public DateSelector() {
InitializeComponent();
}
private void OnDateSelected(object? sender, DateChangedEventArgs e) {
var date = DateOnly.FromDateTime(CurrentDate);
Command?.Execute(date);
}
}

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:components="clr-namespace:WorkTime.Mobile.Views.Components"
xmlns:viewModels="clr-namespace:WorkTime.Mobile.ViewModels"
xmlns:models="clr-namespace:WorkTime.Shared.Models;assembly=WorkTime.Shared"
x:Class="WorkTime.Mobile.Views.Pages.CapturePage"
x:DataType="viewModels:CaptureViewModel"
Padding="24, 0, 24, 24">
<Grid RowDefinitions="Auto,*,Auto">
<components:DateSelector
Grid.Row="0"
Command="{Binding DateSelectedCommand}"/>
<CollectionView
Grid.Row="1"
ItemsSource="{Binding Entries}">
<CollectionView.ItemTemplate>
<DataTemplate
x:DataType="models:TimeEntry">
<HorizontalStackLayout Spacing="5">
<Label Text="{Binding Timestamp}" />
<Label Text="{Binding Type}" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Button
Grid.Row="2"
Text="{Binding CurrentActionName}"
Command="{Binding RegisterEntryCommand}"/>
</Grid>
</ContentPage>

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WorkTime.Mobile.ViewModels;
namespace WorkTime.Mobile.Views.Pages;
public partial class CapturePage : ContentPage {
public CapturePage(CaptureViewModel model) {
InitializeComponent();
BindingContext = model;
}
}

View File

@@ -1,76 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>WorkTime.Mobile</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- Display name -->
<ApplicationTitle>Zeiterfassung</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>de.leon-hoppe.worktime.mobile</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/>
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128"/>
<!-- Images -->
<MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185"/>
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/>
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AathifMahir.Maui.MauiIcons.Material" Version="4.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WorkTime.Shared\WorkTime.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,110 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ServiceDiscovery;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
// This project should be referenced by each service project in your solution.
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
public static class Extensions {
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder {
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http => {
// Turn on resilience by default
http.AddStandardResilienceHandler();
// Turn on service discovery by default
http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });
return builder;
}
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder {
builder.Logging.AddOpenTelemetry(logging => {
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => {
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing => {
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation()
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
//.AddGrpcClientInstrumentation()
.AddHttpClientInstrumentation();
});
builder.AddOpenTelemetryExporters();
return builder;
}
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder {
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter) {
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
//{
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}
return builder;
}
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder {
builder.Services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app) {
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment()) {
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks("/health");
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks("/alive", new HealthCheckOptions {
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
}

View File

@@ -1,98 +0,0 @@
using Microsoft.AspNetCore.Http;
namespace WorkTime.ServiceDefaults.Results;
public interface ILogicResult<TResult, TError> {
public TResult? Result { get; init; }
public TError? Error { get; init; }
public bool IsSuccessful => Result is not null && Error is null;
IResult MapResult();
}
public interface ILogicResult<TResult> : ILogicResult<TResult, IResult>;
public interface ILogicResult : ILogicResult<object>;
public readonly struct LogicResult<TResult, TError> : ILogicResult<TResult, TError> {
public TResult? Result { get; init; }
public TError? Error { get; init; }
public bool IsSuccessful => Result is not null && Error is null;
public static implicit operator LogicResult<TResult, TError>(TResult result) {
return new LogicResult<TResult, TError> {
Result = result
};
}
public static implicit operator LogicResult<TResult, TError>(TError error) {
return new LogicResult<TResult, TError> {
Error = error
};
}
public IResult MapResult() {
if (!IsSuccessful)
return Microsoft.AspNetCore.Http.Results.Problem();
return Microsoft.AspNetCore.Http.Results.Ok(Result);
}
}
public readonly struct LogicResult<TResult> : ILogicResult<TResult> {
public TResult? Result { get; init; }
public IResult? Error { get; init; }
public bool IsSuccessful => Error is null;
public static implicit operator LogicResult<TResult>(TResult result) {
return new LogicResult<TResult> {
Result = result
};
}
public LogicResult() { }
public LogicResult(IResult error) {
Error = error;
}
public IResult MapResult() {
if (!IsSuccessful) {
return Error!;
}
return Microsoft.AspNetCore.Http.Results.Ok(Result);
}
}
public readonly struct LogicResult : ILogicResult {
public object? Result { get; init; }
public IResult? Error { get; init; }
public bool IsSuccessful => Error is null;
public LogicResult() { }
public LogicResult(IResult error) {
Error = error;
}
public IResult MapResult() {
if (!IsSuccessful) {
return Error!;
}
return Microsoft.AspNetCore.Http.Results.Ok(Result);
}
}

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.0.0"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0"/>
</ItemGroup>
</Project>

View File

@@ -1,24 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WorkTime.Shared.Models;
public class TimeEntry {
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public required Guid Owner { get; set; }
public required TimeEntryType Type { get; set; }
public DateTime Timestamp { get; set; } = DateTime.Now;
public bool IsMoba { get; set; }
}
public enum TimeEntryType {
Login,
Logout,
LoginDrive,
LogoutDrive
}

View File

@@ -1,17 +0,0 @@
using WorkTime.Shared.Models;
namespace WorkTime.Shared.Repositories;
public interface IEntryRepository {
Task<TimeEntry[]> GetAllEntries(Guid owner);
Task<TimeEntry[]> GetEntries(Guid owner, DateOnly date);
Task<TimeEntry?> GetEntry(int id);
Task AddEntry(TimeEntry entry);
Task DeleteEntry(TimeEntry entry);
}

View File

@@ -1,11 +0,0 @@
namespace WorkTime.Shared.Services;
public interface IAuthService {
public const string HeaderName = "Authentication";
public Task<bool> IsAuthenticated();
public Task<Guid> GetCurrentUserId();
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More