Compare commits

..

No commits in common. "main" and "1.1.0" have entirely different histories.
main ... 1.1.0

32 changed files with 1492 additions and 4669 deletions

36
.metadata Normal file
View file

@ -0,0 +1,36 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: android
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: ios
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: web
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View file

@ -1,22 +1,5 @@
FROM archlinux
WORKDIR /app
RUN pacman --noconfirm -Syu
ARG FIREBASE_KEY
ARG FIREBASE_MESSAGING
ARG FIREBASE_STORAGE
ARG FIREBASE_APPID
ARG FIREBASE_AUTH
ARG FIREBASE_ID
ENV FIREBASE_KEY=$FIREBASE_KEY
ENV FIREBASE_MESSAGING=$FIREBASE_MESSAGING
ENV FIREBASE_STORAGE=$FIREBASE_STORAGE
ENV FIREBASE_APPID=$FIREBASE_APPID
ENV FIREBASE_AUTH=$FIREBASE_AUTH
ENV FIREBASE_ID=$FIREBASE_ID
FROM passsy/flutterw:base-latest
COPY . .
RUN pacman --noconfirm -S nodejs npm git base-devel unzip
RUN npm i -g pnpm
RUN ./flutterw config --no-analytics
RUN cd api && pnpm i && pnpm run build
ENTRYPOINT PORT=80 node /app/api/build/index.js
ENTRYPOINT ./flutterw run --release --web-port=80 --web-hostname 0.0.0.0 -d web-server
EXPOSE 80

View file

@ -19,7 +19,7 @@ Vydáno pod licencí AGPL verze 3
1. Clone
2. Build
## Obrázky (verze 1.x)
## Obrázky
![Screenshot přihlašovací obrazovky](images/01.png)
![Screenshot přihlašovací obrazovky](images/02.png)
![Screenshot přihlašovací obrazovky](images/03.png)

View file

@ -1,25 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"overrides": [
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
},
"ignorePatterns":[
"src/public"
]
}

181
api/.gitignore vendored
View file

@ -1,181 +0,0 @@
# Created by https://www.toptal.com/developers/gitignore/api/linux,visualstudiocode,node
# Edit at https://www.toptal.com/developers/gitignore?templates=linux,visualstudiocode,node
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/linux,visualstudiocode,node
build
src/public/*
!src/public/.gitkeep

View file

@ -1,26 +0,0 @@
# Tourdeappka NodeJS API server
(c) 2023 Matyáš Caras a Richard Pavlikán
## Požadavky
- [NodeJS](https://nodejs.org) LTS verze (16+)
- [pnpm](https://pnpm.io)
## Jak spustit
1. Nainstaluj NodeJS
2. Nainstaluj pNPM
3. Stáhni repozitář
4. Nainstaluj závislosti (`pnpm i`)
### K vývoji
5. Vytvoř soubor `.env`:
```js
FIREBASE_KEY=klic
FIREBASE_AUTH=nejakaurl
FIREBASE_ID=idcko
FIREBASE_STORAGE=nejakaurl
FIREBASE_MESSAGING=idcko
FIREBASE_APPID=idcko
```
6. Spusť pomocí `pnpm run dev`
### Live server
5. Ulož proměnné dle předchozí struktury jako systémové proměnné
6. Spusť pomocí `pnpm start`

View file

@ -1,30 +0,0 @@
{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "NODE_DEBUG=true ts-node ./src",
"build": "pnpm run clean && pnpm run flutter && tsc -p tsconfig.json && cp -r ./src/public ./build/public",
"clean": "rm -rf ./build",
"start": "pnpm run build && node ./build",
"flutter": "cd .. && ./flutterw clean && ./flutterw pub get && ./flutterw build web --release && rm -rf ./api/src/public/* && mv ./build/web/* ./api/src/public"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^18.14.0",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"dotenv": "^16.0.3",
"eslint": "^8.34.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
},
"dependencies": {
"@fastify/static": "^6.9.0",
"fastify": "^4.13.0",
"firebase": "^9.17.1"
}
}

2209
api/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,112 +0,0 @@
import { collection, getDocs, Firestore, doc, getDoc, addDoc, deleteDoc, updateDoc } from 'firebase/firestore/lite';
import { Record } from "./models/record"
import { RawData } from "./models/rawdb"
import { NewRecord } from "./models/new_record"
export async function getUserRecord(db: Firestore, user: string, record: string): Promise<Record | null> {
const recordDoc = await getDoc(doc(db, `users`, user, "records", record))
if (recordDoc.exists()) {
const d = recordDoc.data() as RawData
return { "programming_language": d['programming_language'].jazyk, id: recordDoc.id, date: new Date(d.date.seconds), "time_spent": d['time_spent'], rating: d.rating, description: d.descriptionRaw }
}
else {
return null;
}
}
export async function getAllUserRecords(db: Firestore, user: string): Promise<Record[] | null> {
const userDoc = await getDoc(doc(db, `users`, user))
if (!userDoc.exists()) return null;
const records = (await getDocs(collection(db, "users", user, "records")))
const recordArr: Record[] = []
records.forEach(r => {
const d = r.data() as RawData
recordArr.push({
date: d.date.toDate(),
"time_spent": d['time_spent'],
"programming_language": d['programming_language'].jazyk,
rating: d.rating,
description: d.descriptionRaw,
id: r.id
})
})
return recordArr
}
export async function createRecord(db: Firestore, user: string, data: NewRecord): Promise<string | null> {
const userDoc = await getDoc(doc(db, "users", user))
if (userDoc.exists()) {
const docRef = await addDoc(collection(db, "users", user, "records"), { ...data,"time_spentRaw":textToSec(data.time_spent), "description": null })
return docRef.id;
}
else {
return null;
}
}
export async function updateRecord(db: Firestore, user: string, id: string, data: NewRecord): Promise<boolean | null> {
const userDoc = await getDoc(doc(db, "users", user))
if (userDoc.exists()) {
const docRef = await getDoc(doc(db, "users", user, "records", id))
if (!docRef.exists()) return false;
await updateDoc(docRef.ref, {...data,"time_spentRaw":textToSec(data.time_spent)})
return true;
}
else {
return null;
}
}
export async function deleteRecord(db: Firestore, user: string, rec: string): Promise<boolean | null> {
const userDoc = await getDoc(doc(db, "users", user))
if (!userDoc.exists()) return null;
const recordDoc = await getDoc(doc(db, "users", user, "records", rec))
if (!recordDoc.exists()) return false
await deleteDoc(recordDoc.ref)
return true;
}
function textToSec(vstup: string):number|undefined {
const regex = /(\d+) hodin(?: |y |a )(\d+) minut(?:$|a$|y$)/gm
let s:number|undefined = 0;
let m;
while ((m = regex.exec(vstup)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
// The result can be accessed through the `m`-variable.
let ok = true;
m.forEach((match, groupIndex) => {
try {
switch (groupIndex) {
case 1:
if(s == undefined){
ok = false;
break;
}
s += parseInt(match) * 3600
break;
case 2:
if(s == undefined){
ok = false;
break;
}
s += parseInt(match) * 60
break;
default:
break;
}
} catch (error) {
ok = false;
}
});
if(!ok) {
s = undefined;
break;
}
}
return s;
}

View file

@ -1,234 +0,0 @@
import fastify from 'fastify'
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore/lite';
import { Record } from "./models/record"
import path from 'path';
import { createRecord, deleteRecord, getAllUserRecords, getUserRecord, updateRecord } from './firebase';
import { Params } from './models/params';
import { fastifyStatic } from "@fastify/static"
import { NewRecord, NewRecordRaw } from './models/new_record';
/*
Copyright (C) 2022 Matyáš Caras a Richard Pavlikán
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
debugme().then(() => {
// Zkontrolovat proměnné
["FIREBASE_KEY", "FIREBASE_AUTH", "FIREBASE_ID", "FIREBASE_STORAGE", "FIREBASE_MESSAGING", "FIREBASE_APPID"].forEach(v => {
if (!Object.keys(process.env).includes(v)) {
throw new Error(`Chybí systémová proměnná '${v}'`)
}
})
const server = fastify()
server.register(fastifyStatic, {
root: path.join(__dirname, "public")
})
// Konfigurace pro napojení na Firebase
const firebaseConfig = {
apiKey: process.env["FIREBASE_KEY"],
authDomain: process.env["FIREBASE_AUTH"],
projectId: process.env["FIREBASE_ID"],
storageBucket: process.env["FIREBASE_STORAGE"],
messagingSenderId: process.env["FIREBASE_MESSAGING"],
appId: process.env["FIREBASE_APPID"]
};
// Připojit se na Firebase
const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);
// Registrovat routy
server.get("/", (req, res) => {
res.sendFile("index.html")
})
// API routy
// Získat jeden záznam uživatele ID
server.get('/users/:userid/records/:recordid', async (req, res) => {
if ((req.params as Params).userid == "" || (req.params as Params).recordid == "") return res.status(400).type("application/json").send(JSON.stringify({ "error": "Parametry nesmí být prázdné", "status": "error" }))
const record: Record | null = await getUserRecord(db, (req.params as Params).userid as string, (req.params as Params).recordid as string)
if (!record) return res.status(404).type("application/json").send(JSON.stringify({ "error": "Uživatel neexistuje", "status": "error" }))
return res.type("application/json").send(JSON.stringify(record))
})
// Smazat jeden záznam
server.delete("/users/:userid/records/:recordid", async (req, res) => {
if ((req.params as Params).userid == "" || (req.params as Params).recordid == "") return res.status(400).type("application/json").send(JSON.stringify({ "error": "Parametry nesmí být prázdné", "status": "error" }))
const r = await deleteRecord(db, (req.params as Params).userid as string, (req.params as Params).recordid as string)
if (r == null) {
return res.status(404).type("application/json").send(JSON.stringify({ "status": "error", "message": "Uživatel neexistuje" }))
}
else if (r == false) {
return res.status(404).type("application/json").send(JSON.stringify({ "status": "error", "message": "Záznam neexistuje" }))
}
return res.status(200).type("application/json").send(JSON.stringify({ "status": "OK" }))
})
// Upravit jeden záznam
server.put("/users/:userid/records/:recordid", {
schema: {
body: {
type: 'object',
properties: {
"date": { type: 'string' },
"time_spent": { type: 'string' },
"programming_language": { type: 'string' },
"description": { type: 'string' },
"rating": { type: 'number' }
}
}
}
}, async (req, res) => {
if ((req.params as Params).userid == "" || (req.params as Params).recordid == "") return res.status(400).type("application/json").send(JSON.stringify({ "error": "Parametry nesmí být prázdné", "status": "error" }))
try {
const data = req.body as NewRecordRaw
if (data.rating > 5 || data.rating < 0) return res.status(400).type("application/json").send(JSON.stringify({ "error": "'rating' je mimo interval 0-5", "status": "error" }))
const regex = /(\d+) hodin(?: |y |a )(\d+) minut(?:$|a$|y$)/gm
if(!regex.test(data.time_spent)){
return res.status(400).type("application/json").send(JSON.stringify({ "error": "time_spent není ve správném formátu", "status": "error" }))
}
const jazyky: { jazyk: string, barva: number }[] = [
{ "jazyk": "C#", "barva": 0xff8200f3 },
{ "jazyk": "JavaScript", "barva": 0xfffdd700 },
{ "jazyk": "Python", "barva": 0xff0080ee },
{ "jazyk": "PHP", "barva": 0xff00abff },
{ "jazyk": "C++", "barva": 0xff1626ff },
{ "jazyk": "Kotlin", "barva": 0xffe34b7c },
{ "jazyk": "Java", "barva": 0xfff58219 },
{ "jazyk": "Dart", "barva": 0xff40c4ff },
{ "jazyk": "F#", "barva": 0xff85ddf3 },
{ "jazyk": "Elixir", "barva": 0xff543465 },
{ "jazyk": "Carbon", "barva": 0xff606060 },
];
const j: { jazyk: string, barva: number } = (jazyky.filter((v) => v.jazyk.toLowerCase() == data['programming_language'].toLowerCase()).length > 0) ? jazyky.filter((v) => v.jazyk == data['programming_language'])[0] : { "jazyk": data["programming_language"], "barva": 0xffffffff }
const record: NewRecord = {
date: new Date(data.date), "programming_language": j, "time_spent": data['time_spent'], rating: data.rating,
descriptionRaw: data.description, programmer: (req.params as Params).userid as string
}
const r = await updateRecord(db, (req.params as Params).userid as string, (req.params as Params).recordid as string, record)
if (r == null) {
return res.status(404).type("application/json").send(JSON.stringify({ "status": "error", "message": "Uživatel neexistuje" }))
}
else if(r == false){
return res.status(404).type("application/json").send(JSON.stringify({ "status": "error", "message": "Záznam neexistuje" }))
}
return res.status(200).type("application/json").send(JSON.stringify({ "status": "OK" }))
} catch (error) {
if (process.env["NODE_DEBUG"] == "true") console.log(error)
return res.status(400).type("application/json").send(JSON.stringify({ "error": "Zaslaná data nejsou v platném formátu JSON", "status": "error" }))
}
})
// Získat všechny záznamy uživatele
server.get("/users/:userid/records", async (req, res) => {
if ((req.params as Params).userid == "") return res.status(400).type("application/json").send(JSON.stringify({ "error": "Parametry nesmí být prázdné", "status": "error" }))
const r: Record[] | null = await getAllUserRecords(db, (req.params as Params).userid as string)
if (!r) return res.status(404).type("application/json").send(JSON.stringify({ "status": "error", "message": "Uživatel neexistuje" }))
return res.type("application/json").send(JSON.stringify(
r
))
})
// Vytvořit nový záznam
server.post("/users/:userid/records", {
schema: {
body: {
type: 'object',
properties: {
"date": { type: 'string' },
"time_spent": { type: 'string' },
"programming_language": { type: 'string' },
"description": { type: 'string' },
"rating": { type: 'number' }
}
}
}
}, async (req, res) => {
if ((req.params as Params).userid == "") return res.status(400).type("application/json").send(JSON.stringify({ "error": "Parametry nesmí být prázdné", "status": "error" }))
try {
const data = req.body as NewRecordRaw
if (data.rating > 5 || data.rating < 0) return res.status(400).type("application/json").send(JSON.stringify({ "error": "'rating' je mimo interval 0-5", "status": "error" }))
const regex = /(\d+) hodin(?: |y |a )(\d+) minut(?:$|a$|y$)/gm
if(!regex.test(data.time_spent)){
return res.status(400).type("application/json").send(JSON.stringify({ "error": "time_spent není ve správném formátu", "status": "error" }))
}
const jazyky: { jazyk: string, barva: number }[] = [
{ "jazyk": "C#", "barva": 0xff8200f3 },
{ "jazyk": "JavaScript", "barva": 0xfffdd700 },
{ "jazyk": "Python", "barva": 0xff0080ee },
{ "jazyk": "PHP", "barva": 0xff00abff },
{ "jazyk": "C++", "barva": 0xff1626ff },
{ "jazyk": "Kotlin", "barva": 0xffe34b7c },
{ "jazyk": "Java", "barva": 0xfff58219 },
{ "jazyk": "Dart", "barva": 0xff40c4ff },
{ "jazyk": "F#", "barva": 0xff85ddf3 },
{ "jazyk": "Elixir", "barva": 0xff543465 },
{ "jazyk": "Carbon", "barva": 0xff606060 },
];
const j: { jazyk: string, barva: number } = (jazyky.filter((v) => v.jazyk.toLowerCase() == data['programming_language'].toLowerCase()).length > 0) ? jazyky.filter((v) => v.jazyk == data['programming_language'])[0] : { "jazyk": data["programming_language"], "barva": 0xffffffff }
const record: NewRecord = {
date: new Date(data.date), "programming_language": j, "time_spent": data['time_spent'], rating: data.rating,
descriptionRaw: data.description, programmer: (req.params as Params).userid as string
}
const r:string|null = await createRecord(db, (req.params as Params).userid as string, record)
if (r == null) {
return res.status(404).type("application/json").send(JSON.stringify({ "status": "error", "message": "Uživatel neexistuje" }))
}
return res.status(201).type("application/json").send(JSON.stringify({"date":record.date,"programming_language":record.programming_language.jazyk,"time_spent":record.time_spent,rating:record.rating,description:record.descriptionRaw,id:r} as Record))
} catch (error) {
if (process.env["NODE_DEBUG"] == "true") console.log(error)
return res.status(400).type("application/json").send(JSON.stringify({ "error": "Zaslaná data nejsou v platném formátu JSON", "status": "error" }))
}
})
server.listen({ port: (!process.env["PORT"]) ? 8080 : parseInt(process.env["PORT"]), host: "0.0.0.0" }, (err, address) => {
if (err) {
console.error(err)
process.exit(1)
}
console.log(`Server listening at ${address}`)
})
})
async function debugme() {
if (process.env["NODE_DEBUG"] == "true") {
const dotenv = await import("dotenv");
dotenv.config()
}
return true;
}

View file

@ -1,16 +0,0 @@
export type NewRecord={
date:Date,
"time_spent":string,
"programming_language":{jazyk:string,barva:number},
rating:number,
descriptionRaw:string,
programmer:string
}
export type NewRecordRaw={
date:string,
"time_spent":string,
"programming_language":string,
rating:number,
description:string
}

View file

@ -1,4 +0,0 @@
export type Params={
userid?:string,
recordid?:string
}

View file

@ -1,12 +0,0 @@
import { Timestamp } from "firebase/firestore/lite"
export type RawData = {
"programming_language": {jazyk:string,barva:number},
rating:number,
descriptionRaw: string,
description:unknown[],
date:Timestamp,
programmer:string,
toDate:Timestamp,
"time_spent":string
}

View file

@ -1,8 +0,0 @@
export type Record={
date:Date,
"time_spent":string,
"programming_language":string,
rating:number,
description:string,
id:string
}

View file

@ -1,106 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "CommonJS", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./build", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": [
"src/*",
]
}

View file

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
import 'firebase_options.dart';
import 'firebase_options.dart'; //TODO: Přidejte si vlastní firebase nastavení
/*
Copyright (C) 2022 Matyáš Caras a Richard Pavlikán
@ -27,7 +27,8 @@ import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
options: DefaultFirebaseOptions
.currentPlatform, //TODO: Přidejte si vlastní firebase nastavení
);
runApp(const MyApp());
}
@ -39,7 +40,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return ResponsiveSizer(
builder: (p0, p1, p2) => MaterialApp(
title: 'Kodelog',
title: 'Deník Programátora',
theme: ThemeData(
primarySwatch: Colors.blue,
scaffoldBackgroundColor: Vzhled.backgroundColor,

View file

@ -2,20 +2,40 @@ import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:denikprogramatora/okna/app.dart';
import 'package:denikprogramatora/okna/settings.dart';
import 'package:denikprogramatora/okna/signin_page.dart';
import 'package:denikprogramatora/utils/datum_cas.dart';
import 'package:denikprogramatora/utils/devicecontainer.dart';
import 'package:denikprogramatora/utils/input_decoration.dart';
import 'package:denikprogramatora/utils/loading_widget.dart';
import 'package:denikprogramatora/utils/months.dart';
import 'package:denikprogramatora/utils/my_category.dart';
import 'package:denikprogramatora/utils/my_container.dart';
import 'package:denikprogramatora/utils/new_record_dialog.dart';
import 'package:denikprogramatora/utils/programmer.dart';
import 'package:denikprogramatora/utils/show_info_dialog.dart';
import 'package:denikprogramatora/utils/vzhled.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:url_launcher/url_launcher_string.dart';
/*
Copyright (C) 2022 Matyáš Caras a Richard Pavlikán
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
class AllRecordsPage extends StatefulWidget {
const AllRecordsPage({super.key});
@ -29,7 +49,30 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
int selectedDay = DateTime.now().day;
int year = DateTime.now().year;
List<MyCategory> categories = [MyCategory("Nic", "nic")];
List<Programmer> programmers = [
const Programmer("Nic", "nic"),
Programmer(name, userUid)
];
List filterJazyky = [
{"jazyk": "Nic", "barva": 0xff8200f3},
];
late String selectedCategory;
late String selectedProgrammer;
bool newestToOldest = true;
late String selectedJazyk;
DateTime fromDate =
DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
DateTime toDate =
DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
bool searchByFromDate = false;
bool searchByToDate = false;
int timeHour = 0;
int timeMinute = 0;
int review = 0;
@override
void initState() {
@ -43,14 +86,29 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
return;
}
ref.get().then((value) {
setState(() {
name = FirebaseAuth.instance.currentUser!.displayName ??
value[
"name"]; // fallback když uživatel je vytvořen skrz firebase admin
name = FirebaseAuth.instance.currentUser!.displayName!;
ref.collection("programmers").get().then((value) {
for (var snap in value.docs) {
var data = snap.data();
programmers.add(Programmer(data["name"], snap.id));
}
});
ref.collection("categories").get().then((value) {
for (var snap in value.docs) {
var data = snap.data();
categories.add(MyCategory(data["name"], snap.id));
}
});
filterJazyky.addAll(jazyky);
selectedCategory = categories[0].id;
selectedProgrammer = programmers[0].id;
selectedJazyk = "Nic";
mesic = months[DateTime.now().month - 1];
setState(() {
@ -90,7 +148,7 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
onPressed: () => showAboutDialog(
context: context,
applicationName: "Kodelog",
applicationVersion: "2.0.1",
applicationVersion: "1.1.0",
applicationLegalese:
"©️ 2023 Matyáš Caras a Richard Pavlikán,\n vydáno pod licencí AGPLv3",
children: [
@ -190,19 +248,31 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
Expanded(
child: MyContainer(
width: 90.w,
child: Center(
child: DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width:
(Device.screenType == ScreenType.mobile)
? 80.w
: 40.w,
child: Column(
children: [
const Text("Filtr",
style: Vzhled.nadpis),
const SizedBox(height: 15),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Záznamy seřazené \nod ${newestToOldest ? "nejnovějších po nejstarší" : "nejstarších po nejnovější"}"),
Flexible(
child: Text(
"Záznamy seřazené od ${newestToOldest ? "nejnovějších po nejstarší" : "nejstarších po nejnovější"}"),
),
TextButton(
onPressed: () {
setState(() {
newestToOldest = !newestToOldest;
newestToOldest =
!newestToOldest;
});
},
child: const Text(
@ -212,22 +282,374 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
)
],
),
const SizedBox(height: 5),
const SizedBox(height: 15),
DeviceContainer(
children: [
const Text("Kategorie"),
const SizedBox(width: 15),
DropdownButton(
value: selectedCategory,
items: categories.map((e) {
return DropdownMenuItem(
value: e.id,
child: Text(e.name));
}).toList(),
onChanged: (value) {
setState(() {
selectedCategory = value!;
});
},
),
],
),
const SizedBox(height: 15),
DeviceContainer(
children: [
const Text("Jazyk"),
const SizedBox(width: 15),
DropdownButton(
value: selectedJazyk,
dropdownColor:
Vzhled.backgroundColor,
items: filterJazyky
.map(
(e) => DropdownMenuItem(
value: e["jazyk"],
child: Text(e["jazyk"]),
),
)
.toList(),
onChanged: (value) {
setState(() {
selectedJazyk =
(value as String?)!;
});
},
),
],
),
const SizedBox(height: 15),
DeviceContainer(
children: [
const Text("Programátor"),
const SizedBox(width: 15),
DropdownButton(
value: selectedProgrammer,
items: programmers.map((e) {
return DropdownMenuItem(
value: e.id,
child: Text(e.name));
}).toList(),
onChanged: (value) {
setState(() {
selectedProgrammer = value!;
});
},
),
],
),
const SizedBox(height: 15),
DeviceContainer(
children: [
const Text("Strávený čas"),
const SizedBox(width: 15),
SizedBox(
width: (Device.screenType ==
ScreenType.mobile)
width: 75,
child: TextField(
decoration:
inputDecoration("Hodin"),
onChanged: (value) {
setState(() {
timeHour =
value.trim().isEmpty
? 0
: int.parse(value);
});
},
keyboardType:
TextInputType.number,
inputFormatters: <
TextInputFormatter>[
FilteringTextInputFormatter
.digitsOnly
],
),
),
const SizedBox(width: 15),
SizedBox(
width: 75,
child: TextField(
decoration:
inputDecoration("Minut"),
onChanged: (value) {
setState(() {
timeMinute =
value.trim().isEmpty
? 0
: int.parse(value);
});
},
keyboardType:
TextInputType.number,
inputFormatters: <
TextInputFormatter>[
FilteringTextInputFormatter
.digitsOnly
],
),
),
const SizedBox(width: 15),
if (timeMinute != 0 ||
timeHour != 0)
TextButton(
onPressed: () {
setState(() {
timeMinute = 0;
timeHour = 0;
});
},
child: const Text(
"Zrušit filtr",
style: Vzhled.textBtn,
),
)
],
),
const SizedBox(height: 15),
DeviceContainer(
children: [
const Text("Hodnocení"),
const SizedBox(width: 15),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: List.generate(
5,
(index) {
return IconButton(
onPressed: () {
setState(() {
review = index + 1;
});
},
icon: Icon(Icons.star,
color: (index + 1) <=
review
? Colors.yellow
: Colors.grey),
);
},
),
),
if (review != 0)
TextButton(
onPressed: () {
setState(() {
review = 0;
});
},
child: const Text(
"Zrušit filtr",
style: Vzhled.textBtn,
),
)
],
),
const SizedBox(height: 15),
Row(
children: [
const Text("Od: "),
const SizedBox(width: 15),
TextButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: fromDate,
firstDate: DateTime(
DateTime.now()
.year -
5),
lastDate: DateTime(
DateTime.now()
.year +
5))
.then((value) {
setState(() {
fromDate = value!;
searchByFromDate = true;
});
}).onError(
(error, stackTrace) =>
null);
},
child: Text(searchByFromDate
? "${fromDate.day}.${fromDate.month}.${fromDate.year}"
: "Vybrat den"),
),
const SizedBox(width: 15),
if (searchByFromDate)
TextButton(
onPressed: () {
setState(() {
searchByFromDate = false;
});
},
child: const Text(
("Zrušit filtr"),
style: Vzhled.textBtn,
),
),
],
),
const SizedBox(height: 5),
Row(
children: [
const Text("Do: "),
const SizedBox(width: 15),
TextButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: toDate,
firstDate: DateTime(
DateTime.now()
.year -
5),
lastDate: DateTime(
DateTime.now()
.year +
5))
.then((value) {
setState(() {
toDate = value!;
searchByToDate = true;
});
}).onError(
(error, stackTrace) =>
null);
},
child: Text(searchByToDate
? "${toDate.day}.${toDate.month}.${toDate.year}"
: "Vybrat den"),
),
const SizedBox(width: 15),
if (searchByToDate)
TextButton(
onPressed: () {
setState(() {
searchByToDate = false;
});
},
child: const Text(
("Zrušit filtr"),
style: Vzhled.textBtn,
),
),
],
),
],
),
),
SizedBox(
width:
(Device.screenType == ScreenType.mobile)
? 80.w
: 40.w,
child: StreamBuilder(
stream: ref
.collection("records")
.orderBy("date",
.orderBy("fromDate",
descending: newestToOldest)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var docs = snapshot.data!.docs;
if (selectedProgrammer != "nic") {
docs = docs
.where((element) =>
element
.data()["programmer"] ==
selectedProgrammer)
.toList();
}
if (selectedCategory != "nic") {
docs = docs
.where((element) => (element
.data()[
"categories"] as List)
.contains(selectedCategory))
.toList();
}
if (selectedJazyk != "Nic") {
docs = docs
.where((element) =>
element.data()["language"]
["jazyk"] ==
selectedJazyk)
.toList();
}
if (searchByFromDate) {
docs = docs
.where((d) =>
(d.data()["fromDate"]
as Timestamp)
.toDate()
.compareTo(
fromDate) ==
1 ||
(d.data()["fromDate"]
as Timestamp)
.toDate()
.compareTo(
fromDate) ==
0)
.toList();
}
if (searchByToDate) {
docs = docs
.where((d) =>
(d.data()["toDate"]
as Timestamp)
.toDate()
.compareTo(
toDate) ==
-1 ||
(d.data()["toDate"]
as Timestamp)
.toDate()
.compareTo(
toDate) ==
0)
.toList();
}
if (timeHour != 0 ||
timeMinute != 0) {
if (kDebugMode) {
print(
"${timeHour == 0 ? "" : (timeHour == 1 ? "$timeHour hodina" : "$timeHour hodin")}${timeMinute == 0 ? "" : (timeMinute == 1 ? "a $timeMinute minuta" : " a $timeMinute minut")}");
}
docs = docs
.where((element) => (element
.data()["codingTime"] ==
"${timeHour == 0 ? "" : (timeHour == 1 ? "$timeHour hodina" : "$timeHour hodin")}${timeMinute == 0 ? "" : (timeMinute == 1 ? "a $timeMinute minuta" : " a $timeMinute minut")}"))
.toList();
}
if (review != 0) {
docs = docs
.where((element) =>
element.data()["review"] ==
review)
.toList();
}
return Column(
children: List.generate(
docs.length,
@ -236,8 +658,7 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
return Padding(
padding:
const EdgeInsets.all(
8.0),
const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
borderRadius:
@ -245,43 +666,31 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
.all(
Radius.circular(4),
),
color: Color(data[
"programming_language"]
color: Color(
data["language"]
["barva"]),
),
child: Material(
color:
Colors.transparent,
color: Colors.transparent,
child: InkWell(
onTap: () =>
showInfoDialog(
context,
data,
docs[index]
.id,
name),
docs[index].id),
child: Padding(
padding:
const EdgeInsets
.all(8.0),
child: Row(
children: [
Text((data["date"]
as Timestamp)
.toDate()
.dateString),
Text(
"${(data["fromDate"] as Timestamp).toDate().year}.${(data["fromDate"] as Timestamp).toDate().month}.${(data["fromDate"] as Timestamp).toDate().day} ${(data["fromDate"] as Timestamp).toDate().hour < 10 ? "0${(data["fromDate"] as Timestamp).toDate().hour}" : (data["fromDate"] as Timestamp).toDate().hour}:${(data["fromDate"] as Timestamp).toDate().minute < 10 ? "0${(data["fromDate"] as Timestamp).toDate().minute}" : (data["fromDate"] as Timestamp).toDate().minute}"),
const SizedBox(
width: 20,
),
Text(
"${data["programming_language"]["jazyk"]}",
style: const TextStyle(
fontWeight:
FontWeight
.bold),
),
Text(
" - ${data["time_spent"]}")
" - ${data["language"]["jazyk"]}")
],
),
),
@ -301,7 +710,6 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
),
),
),
),
],
),
)

View file

@ -66,15 +66,10 @@ class _HlavniOknoState extends State<HlavniOkno> {
(route) => false);
return;
}
userUid = FirebaseAuth.instance.currentUser!.uid;
ref.get().then((value) {
setState(() {
name = FirebaseAuth.instance.currentUser!.displayName ??
value[
"name"]; // fallback když uživatel je vytvořen skrz firebase admin
});
});
userUid = FirebaseAuth.instance.currentUser!.uid;
name = FirebaseAuth.instance.currentUser!.displayName!;
mesic = months[DateTime.now().month - 1];
setState(() {
@ -113,9 +108,9 @@ class _HlavniOknoState extends State<HlavniOkno> {
onPressed: () => showAboutDialog(
context: context,
applicationName: "Kodelog",
applicationVersion: "2.0.1",
applicationVersion: "1.1.0",
applicationLegalese:
"©️ 2023 Matyáš Caras a Richard Pavlikán" /*+",\n vydáno pod licencí AGPLv3"*/,
"©️ 2023 Matyáš Caras a Richard Pavlikán,\n vydáno pod licencí AGPLv3",
children: [
TextButton(
child: const Text("Zdrojový kód"),
@ -149,8 +144,8 @@ class _HlavniOknoState extends State<HlavniOkno> {
),
MyContainer(
width: (Device.screenType == ScreenType.mobile)
? 90.w
: 40.w,
? 95.w
: 45.w,
child: DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.spaceBetween,
@ -225,10 +220,17 @@ class _HlavniOknoState extends State<HlavniOkno> {
if (snapshot.hasData) {
var docs = snapshot.data!.docs;
var jenMesic = docs
.where((d) => DateTime.parse(
.where((d) =>
DateTime.parse(
"$year-${(mesic.position + 1 < 10) ? "0${mesic.position + 1}" : mesic.position + 1}-${selectedDay < 10 ? "0$selectedDay" : selectedDay} 00:00:00")
.isAtSameMomentAs(
(d.data()["date"]
.isBefore(
(d.data()["toDate"]
as Timestamp)
.toDate()) &&
DateTime.parse(
"$year-${(mesic.position + 1 < 10) ? "0${mesic.position + 1}" : mesic.position + 1}-${selectedDay < 10 ? "0$selectedDay" : selectedDay} 23:59:59")
.isAfter(
(d.data()["fromDate"]
as Timestamp)
.toDate()))
.toList() // vybere pouze záznamy, které probíhají ve vybraný den
@ -260,8 +262,8 @@ class _HlavniOknoState extends State<HlavniOkno> {
.all(
Radius.circular(4),
),
color: Color(data[
"programming_language"]
color: Color(
data["language"]
["barva"]),
),
child: Material(
@ -272,8 +274,7 @@ class _HlavniOknoState extends State<HlavniOkno> {
context,
data,
jenMesic[index]
.id,
name),
.id),
child: Padding(
padding:
const EdgeInsets
@ -281,14 +282,12 @@ class _HlavniOknoState extends State<HlavniOkno> {
child: Row(
children: [
Text(
"${data["programming_language"]["jazyk"]}",
style: const TextStyle(
fontWeight:
FontWeight
.bold),
"${(data["fromDate"] as Timestamp).toDate().hour < 10 ? "0${(data["fromDate"] as Timestamp).toDate().hour}" : (data["fromDate"] as Timestamp).toDate().hour}:${(data["fromDate"] as Timestamp).toDate().minute < 10 ? "0${(data["fromDate"] as Timestamp).toDate().minute}" : (data["fromDate"] as Timestamp).toDate().minute}"),
const SizedBox(
width: 20,
),
Text(
" - ${data["time_spent"]}")
" - ${data["language"]["jazyk"]}")
],
),
),
@ -309,10 +308,7 @@ class _HlavniOknoState extends State<HlavniOkno> {
height: 50,
),
SizedBox(
width:
(Device.screenType == ScreenType.mobile)
? 60.w
: 40.w,
width: 45.w,
child: Column(
children: [
DeviceContainer(
@ -508,10 +504,9 @@ class _HlavniOknoState extends State<HlavniOkno> {
: 5),
(index) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
// (Device.screenType == ScreenType.mobile)
// ? MainAxisAlignment.center
// : MainAxisAlignment.start,
mainAxisAlignment: (Device.screenType == ScreenType.mobile)
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: List.generate(
(Device.screenType == ScreenType.mobile) ? 3 : 7,
(index) {

View file

@ -1,21 +1,34 @@
import 'package:denikprogramatora/okna/app.dart';
import 'package:denikprogramatora/okna/signin_page.dart';
import 'package:denikprogramatora/okna/users_page.dart';
import 'package:denikprogramatora/utils/devicecontainer.dart';
import 'package:denikprogramatora/utils/loading_widget.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:url_launcher/url_launcher_string.dart';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import '../utils/csv.dart';
import '../utils/my_container.dart';
import '../utils/new_record_dialog.dart';
import '../utils/vzhled.dart';
import 'all_records.dart';
/*
Copyright (C) 2022 Matyáš Caras a Richard Pavlikán
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
class NastaveniOkno extends StatefulWidget {
const NastaveniOkno({super.key});
@ -26,8 +39,6 @@ class NastaveniOkno extends StatefulWidget {
class _NastaveniOknoState extends State<NastaveniOkno> {
var _loading = true;
var name = "error";
bool isAdmin = false;
@override
void initState() {
super.initState();
@ -38,16 +49,7 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
(route) => false);
return;
}
ref.get().then((value) {
setState(() {
name = FirebaseAuth.instance.currentUser!.displayName ??
value[
"name"]; // fallback když uživatel je vytvořen skrz firebase admin
isAdmin = value["isAdmin"];
});
});
name = FirebaseAuth.instance.currentUser!.displayName!;
setState(() {
_loading = false;
@ -64,11 +66,9 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
height: 100.h,
child: (_loading)
? const LoadingWidget()
: Column(
children: [
: Column(children: [
DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.spaceBetween,
mainAxisAlignmentDesktop: MainAxisAlignment.spaceBetween,
children: [
MyContainer(
width: (Device.screenType == ScreenType.mobile)
@ -82,7 +82,7 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
onPressed: () => showAboutDialog(
context: context,
applicationName: "Kodelog",
applicationVersion: "2.0.1",
applicationVersion: "1.1.0",
applicationLegalese:
"©️ 2023 Matyáš Caras a Richard Pavlikán,\n vydáno pod licencí AGPLv3",
children: [
@ -160,8 +160,7 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
),
const SizedBox(height: 5),
OutlinedButton(
onPressed: () =>
showCreateItemDialog(context),
onPressed: () => showCreateItemDialog(context),
style: Vzhled.orangeCudlik,
child: const Text(
"Přidat záznam",
@ -186,18 +185,19 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
10,
),
),
color: Vzhled.purple),
color: Vzhled.purple,
),
width: 400,
child: InkWell(
onTap: () => showEditJazyk(),
onTap: () => showProgrammersDialog(context,
jenMenit: true),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Oblíbený jazyk",
"Upravit programátory",
style: Vzhled.nadpis,
)
],
@ -223,8 +223,7 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Upravit kategorie",
@ -238,12 +237,6 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
const SizedBox(
height: 15,
),
DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.center,
mainAxisAlignmentMobile:
MainAxisAlignment.center,
children: [
Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
@ -254,36 +247,14 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
color: Vzhled.purple),
width: 400,
child: InkWell(
onTap: () async {
var csv = await exportCsv();
var blob = html.Blob([csv]);
var url =
html.Url.createObjectUrlFromBlob(
blob);
var anchor = html.document
.createElement('a')
as html.AnchorElement
..href = url
..style.display = 'none'
..download =
'db_${name.replaceAll(" ", "_")}.csv';
html.document.body!.children
.add(anchor);
anchor.click();
html.document.body!.children
.remove(anchor);
html.Url.revokeObjectUrl(url);
},
onTap: () => showEditJazyk(),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Exportovat CSV",
"Oblíbený jazyk",
style: Vzhled.nadpis,
)
],
@ -291,112 +262,11 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
),
),
),
const SizedBox(
width: 10,
height: 10,
),
Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(
10,
),
),
color: Vzhled.purple),
width: 400,
child: InkWell(
onTap: () async {
try {
var p = await importCsv(name);
if (p != -1) {
if (!mounted) return;
showDialog(
context: context,
builder: (c) => AlertDialog(
title: const Text("Úspěch!"),
content: Text(
"Importováno $p záznamů"),
actions: [
TextButton(
onPressed: () =>
Navigator.of(c).pop(),
child: const Text("Ok"))
],
),
);
}
} catch (e) {
showDialog(
context: context,
builder: (c) => AlertDialog(
title: const Text(
"Při importování nastala chyba!"),
content: Text(e.toString()),
),
);
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: const [
Text(
"Importovat CSV",
style: Vzhled.nadpis,
)
],
),
),
),
),
],
),
const SizedBox(
height: 15,
),
if (isAdmin)
Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(
10,
),
),
color: Vzhled.purple),
width: 400,
child: InkWell(
onTap: () => Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const UsersPage(),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: const [
Text(
"Správa uživatelů",
style: Vzhled.nadpis,
)
],
),
),
),
),
],
),
),
)
],
),
),
),
]),
))
]),
),
)),
);
}
@ -431,7 +301,6 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
return const LoadingWidget();
}),
),
),
);
));
}
}

View file

@ -9,6 +9,23 @@ import 'package:flutter/scheduler.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:url_launcher/url_launcher_string.dart';
/*
Copyright (C) 2022 Matyáš Caras a Richard Pavlikán
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
class SignInPage extends StatefulWidget {
const SignInPage({super.key});
@ -17,10 +34,8 @@ class SignInPage extends StatefulWidget {
}
class _SignInPageState extends State<SignInPage> {
bool showSignIn = true;
bool isLoading = true;
bool isSignInWidget = true;
bool showEmailPage = true;
String oldDocId = "";
TextEditingController emailCon = TextEditingController();
TextEditingController passwordCon = TextEditingController();
@ -55,7 +70,7 @@ class _SignInPageState extends State<SignInPage> {
: Stack(
children: [
Center(
child: isSignInWidget ? signInWidget() : registerWidget(),
child: showSignIn ? signInWidget() : registerWidget(),
),
Positioned(
bottom: 10,
@ -64,7 +79,7 @@ class _SignInPageState extends State<SignInPage> {
onPressed: () => showAboutDialog(
context: context,
applicationName: "Kodelog",
applicationVersion: "2.0.1",
applicationVersion: "1.1.0",
applicationLegalese:
"©️ 2023 Matyáš Caras a Richard Pavlikán,\n vydáno pod licencí AGPLv3",
children: [
@ -106,7 +121,7 @@ class _SignInPageState extends State<SignInPage> {
SizedBox(
width: (Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
child: TextFormField(
decoration: Vzhled.inputDecoration("E-mail nebo Username"),
decoration: Vzhled.inputDecoration("E-mail"),
cursorColor: Vzhled.textColor,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
@ -119,6 +134,9 @@ class _SignInPageState extends State<SignInPage> {
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
} else if (!RegExp(r'[\w\.]+@[a-z0-9]+\.[a-z]{1,3}')
.hasMatch(value)) {
return "Neplatný e-mail!";
}
return null;
},
@ -170,15 +188,11 @@ class _SignInPageState extends State<SignInPage> {
TextButton(
onPressed: () {
setState(() {
isSignInWidget = false;
showEmailPage = true;
showSignIn = false;
});
},
child: const Text(
"Přihlašuji se poprvé",
style: Vzhled.textBtn,
),
),
child: const Text("Registrovat se", style: Vzhled.textBtn),
)
],
),
),
@ -187,9 +201,7 @@ class _SignInPageState extends State<SignInPage> {
Widget registerWidget() {
GlobalKey<FormState> form = GlobalKey<FormState>();
return MyContainer(
height: 70.h,
width: (Device.screenType == ScreenType.mobile) ? 80.w : 40.w,
child: Form(
key: form,
@ -204,33 +216,17 @@ class _SignInPageState extends State<SignInPage> {
const SizedBox(
height: 30,
),
showEmailPage
? SizedBox(
width:
(Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
SizedBox(
width: (Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
child: TextFormField(
decoration: Vzhled.inputDecoration("Váš e-mail"),
decoration: Vzhled.inputDecoration("Jméno"),
cursorColor: Vzhled.textColor,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
controller: emailCon,
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
controller: nameCon,
onFieldSubmitted: (_) {
if (form.currentState!.validate()) {
signUp();
}
return null;
},
),
)
: SizedBox(
width:
(Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
child: TextFormField(
decoration: Vzhled.inputDecoration("Váš nové heslo"),
cursorColor: Vzhled.textColor,
autocorrect: false,
obscureText: true,
controller: passwordCon,
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
@ -242,104 +238,97 @@ class _SignInPageState extends State<SignInPage> {
const SizedBox(
height: 20,
),
showEmailPage
? OutlinedButton(
style: Vzhled.orangeCudlik,
onPressed: () async {
SizedBox(
width: (Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
child: TextFormField(
decoration: Vzhled.inputDecoration("E-mail"),
cursorColor: Vzhled.textColor,
controller: emailCon,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
onFieldSubmitted: (_) {
if (form.currentState!.validate()) {
var emailRef = await FirebaseFirestore.instance
.collection("users")
.where("email", isEqualTo: emailCon.text)
.get();
if (emailRef.docs.isEmpty) {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text(
"Účet s tímto emailem neexistuje. Pro přístup do aplikace musí váš email přidat admin aplikace."),
),
);
return;
}
setState(() {
oldDocId = emailRef.docs.first.id;
showEmailPage = false;
});
signUp();
}
},
child: const Text("Pokračovat"),
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
} else if (!RegExp(r'[\w\.]+@[a-z0-9]+\.[a-z]{1,3}')
.hasMatch(value)) {
return "Neplatný e-mail!";
}
return null;
},
),
),
const SizedBox(
height: 20,
),
SizedBox(
width: (Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
child: TextFormField(
decoration: Vzhled.inputDecoration("Heslo"),
cursorColor: Vzhled.textColor,
obscureText: true,
controller: passwordCon,
onFieldSubmitted: (_) {
if (form.currentState!.validate()) {
signUp();
}
},
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
}
return null;
},
),
),
const SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Oblíbený jazyk:",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 5),
DropdownButton(
value: jazyk,
dropdownColor: Vzhled.backgroundColor,
items: jazyky
.map(
(e) => DropdownMenuItem(
value: e["jazyk"],
child: Text(e["jazyk"]),
),
)
: OutlinedButton(
.toList(),
onChanged: (value) {
setState(() {
jazyk = (value as String?)!;
});
},
),
],
),
const SizedBox(
height: 20,
),
OutlinedButton(
style: Vzhled.orangeCudlik,
onPressed: () async {
if (form.currentState!.validate()) {
await FirebaseAuth.instance
.createUserWithEmailAndPassword(
email: emailCon.text,
password: passwordCon.text)
.then((value) async {
await FirebaseFirestore.instance
.collection("users")
.doc(oldDocId)
.get()
.then((value) {
FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.set({
"name": value["name"],
"favourite": value["favourite"],
"isAdmin": value["isAdmin"],
"username": value["username"],
"email": value["email"],
});
}).then((value) {
FirebaseFirestore.instance
.collection("users")
.doc(oldDocId)
.delete();
});
// ignore: use_build_context_synchronously
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const HlavniOkno(),
),
);
}).onError((e, st) {
if (e
.toString()
.contains("firebase_auth/email-already-in-use")) {
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text(
"Váš účet už existuje, prosím přihlaste se"),
),
);
} else if (e
.toString()
.contains("firebase_auth/wrong-password")) {
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text("Zadáváte špatné heslo!"),
),
);
}
return;
});
signUp();
}
},
style: Vzhled.orangeCudlik,
child: const Text("Pokračovat")),
child: const Text("Registrovat se"),
),
const SizedBox(
height: 10,
),
@ -350,54 +339,21 @@ class _SignInPageState extends State<SignInPage> {
TextButton(
onPressed: () {
setState(() {
isSignInWidget = true;
showSignIn = true;
});
},
child: const Text(
"Účet mám vytvořen, chci se přihlásit",
style: Vzhled.textBtn,
),
),
child: const Text("Přihlásit se", style: Vzhled.textBtn),
)
],
),
),
);
}
void signIn() async {
setState(() {
isLoading = true;
});
String email = emailCon.text;
if (!emailCon.text.contains("@")) {
var usernameRef = await FirebaseFirestore.instance
.collection("users")
.where("username", isEqualTo: emailCon.text)
.get();
if (usernameRef.docs.isEmpty) {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text("Účet s tímto jménem neexistuje"),
),
);
setState(() {
isLoading = false;
});
return;
}
email = usernameRef.docs.single.data()["email"];
}
void signIn() {
FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: passwordCon.text)
.signInWithEmailAndPassword(
email: emailCon.text, password: passwordCon.text)
.then(
(value) => Navigator.pushReplacement(
context,
@ -432,11 +388,49 @@ class _SignInPageState extends State<SignInPage> {
debugPrint(e.toString());
}
});
}
if (mounted) {
setState(() {
isLoading = false;
void signUp() {
FirebaseAuth.instance
.createUserWithEmailAndPassword(
email: emailCon.text, password: passwordCon.text)
.then(
(value) async {
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.set({
"name": nameCon.text,
"email": emailCon.text,
"favourite": jazyk,
});
value.user?.updateDisplayName(nameCon.text);
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (c) => const HlavniOkno()),
);
},
).onError((error, stackTrace) {
if (error.toString().contains("firebase_auth/email-already-in-use")) {
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text("E-mail je již zaregistrovaný"),
),
);
} else {
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text("Nastala neznámá chyba."),
),
);
debugPrint(error.toString());
}
});
}
}
}

View file

@ -1,633 +0,0 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:denikprogramatora/okna/all_records.dart';
import 'package:denikprogramatora/okna/signin_page.dart';
import 'package:denikprogramatora/utils/devicecontainer.dart';
import 'package:denikprogramatora/utils/loading_widget.dart';
import 'package:denikprogramatora/utils/my_container.dart';
import 'package:denikprogramatora/utils/new_record_dialog.dart';
import 'package:denikprogramatora/utils/really_delete.dart';
import 'package:denikprogramatora/utils/vzhled.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'app.dart';
class UsersPage extends StatefulWidget {
const UsersPage({super.key});
@override
State<UsersPage> createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
var _loading = true;
var name = "error";
@override
void initState() {
super.initState();
if (FirebaseAuth.instance.currentUser == null) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (c) => const SignInPage()),
(route) => false);
return;
}
ref.get().then((value) {
setState(() {
name = FirebaseAuth.instance.currentUser!.displayName ??
value[
"name"]; // fallback když uživatel je vytvořen skrz firebase admin
});
});
setState(() {
_loading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Center(
child: SizedBox(
width: 90.w,
height: 100.h,
child: (_loading)
? const LoadingWidget()
: Column(
children: [
DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.spaceBetween,
children: [
MyContainer(
width: (Device.screenType == ScreenType.mobile)
? 90.w
: 35.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (name != "error") Text("Ahoj $name"),
TextButton(
onPressed: () => showAboutDialog(
context: context,
applicationName: "Kodelog",
applicationVersion: "2.0.1",
applicationLegalese:
"©️ 2023 Matyáš Caras a Richard Pavlikán,\n vydáno pod licencí AGPLv3",
children: [
TextButton(
child: const Text("Zdrojový kód"),
onPressed: () => launchUrlString(
"https://github.com/Royal-Buccaneers/kodelog"),
)
]),
child: const Text(
"Licence",
style: Vzhled.textBtn,
),
),
TextButton(
onPressed: () async {
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (c) => const SignInPage()),
(route) => false);
},
child: const Text(
"Odhlásit se",
style: Vzhled.textBtn,
),
)
],
),
),
MyContainer(
width: (Device.screenType == ScreenType.mobile)
? 90.w
: 40.w,
child: DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => Navigator.of(context)
.pushReplacement(MaterialPageRoute(
builder: (context) =>
const HlavniOkno())),
child: const Text(
"Denní přehled",
style: TextStyle(color: Vzhled.textColor),
),
),
const SizedBox(height: 5),
TextButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
const AllRecordsPage()));
},
child: const Text(
"Všechny\nzáznamy",
style: TextStyle(color: Vzhled.textColor),
),
),
const SizedBox(height: 5),
TextButton(
onPressed: () {},
child: const Text(
"Nastavení",
style: TextStyle(
color: Vzhled.textColor,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 5),
OutlinedButton(
onPressed: () =>
showCreateItemDialog(context),
style: Vzhled.orangeCudlik,
child: const Text(
"Přidat záznam",
),
)
],
),
),
],
),
const SizedBox(height: 5),
Expanded(
child: MyContainer(
width: 90.w,
child: SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
const Text("Uživatelé",
style: Vzhled.mensiAleVelkyText),
OutlinedButton(
onPressed: () => showNewUser(),
style: Vzhled.orangeCudlik,
child: const Text(
"Přidat uživatele",
),
),
],
),
const SizedBox(height: 40),
const Padding(
padding: EdgeInsets.only(right: 8.0, left: 8),
child: DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.spaceBetween,
mainAxisAlignmentMobile:
MainAxisAlignment.center,
children: [
SizedBox(
width: 100,
child: Text(
"Jméno",
),
),
SizedBox(
width: 150,
child: Text(
"Email",
),
),
SizedBox(
width: 150,
child: Text("Username"),
),
SizedBox(
width: 50,
child: Text("Admin"),
),
Text("Nastavení")
],
),
),
const SizedBox(height: 10),
SizedBox(
width: 80.w,
child: const Divider(color: Vzhled.purple),
),
const SizedBox(height: 20),
SingleChildScrollView(
child: StreamBuilder(
stream: FirebaseFirestore.instance
.collection("users")
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Text(
"Nastal error :C...");
} else if (snapshot.hasData) {
var docs = snapshot.data!.docs;
return Column(
children: List.generate(
docs.length,
(index) {
var data = docs[index].data();
return Padding(
padding:
const EdgeInsets.all(8.0),
child: DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment
.spaceBetween,
mainAxisAlignmentMobile:
MainAxisAlignment
.center,
children: [
SizedBox(
width: 100,
child: Text(
data["name"],
style: const TextStyle(
fontWeight:
FontWeight
.bold),
),
),
SizedBox(
width: 150,
child: Text(
data["email"],
),
),
SizedBox(
width: 150,
child: Text(
data["username"] ??
"Prozatím nic"),
),
SizedBox(
width: 50,
child: Icon(
data["isAdmin"] ??
false
? Icons.done
: Icons
.do_disturb),
),
(data["username"] !=
"admin")
? PopupMenuButton(
onSelected:
(value) {
switch (value) {
case 'Změnit práva':
showPrava(
docs[index]
.id,
data[
"isAdmin"]);
break;
case "Upravit username":
showUpravitUsername(
docs[index]
.id,
data[
"username"]);
break;
case 'Odstranit':
showOdstranit(
docs[index]
.id);
break;
}
},
itemBuilder:
(BuildContext
context) {
return {
'Změnit práva',
"Upravit username",
'Odstranit'
}.map((String
choice) {
return PopupMenuItem<
String>(
value:
choice,
child: Text(
choice),
);
}).toList();
},
)
: const SizedBox(
width: 40)
],
),
);
},
),
);
}
return const LoadingWidget();
}),
),
],
),
),
),
),
],
),
),
),
),
);
}
showNewUser() async {
GlobalKey<FormState> key = GlobalKey<FormState>();
String jmeno = "";
String email = "";
String username = "";
bool isAdmin = false;
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Nový uživatel", style: Vzhled.velkyText),
scrollable: true,
content: StatefulBuilder(
builder: (context, setState) {
return SizedBox(
width: 50.w,
child: Form(
key: key,
child: Column(
children: [
TextFormField(
decoration: Vzhled.inputDecoration("Jméno"),
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
}
return null;
},
onChanged: (value) {
jmeno = value;
},
),
const SizedBox(height: 10),
TextFormField(
decoration: Vzhled.inputDecoration("Username"),
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
}
return null;
},
onChanged: (value) {
username = value;
},
),
const SizedBox(height: 10),
TextFormField(
decoration: Vzhled.inputDecoration("Email"),
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
} else if (!RegExp(r'[\w\.]+@[a-z0-9]+\.[a-z]{1,3}')
.hasMatch(value)) {
return "Neplatný e-mail!";
}
return null;
},
onChanged: (value) {
email = value;
},
),
const SizedBox(height: 10),
DropdownButton(
value: isAdmin,
items: ["Admin", "Uživatel"]
.map((e) => DropdownMenuItem(
value: e == "Admin" ? true : false,
child: Text(e)))
.toList(),
onChanged: (value) {
setState(() {
isAdmin = value!;
});
},
),
const SizedBox(height: 20),
OutlinedButton(
style: Vzhled.orangeCudlik,
onPressed: () async {
if (key.currentState!.validate()) {
// kontrola ci niekto nevyuziva username
var usernameRef = await FirebaseFirestore.instance
.collection("users")
.where("username", isEqualTo: username)
.get();
if (usernameRef.docs.isNotEmpty) {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text(
"Toto username patří jinému uživateli, prosím napište nové username."),
),
);
return;
}
// kontrola ci niekto nevyuziva email
var emailRef = await FirebaseFirestore.instance
.collection("users")
.where("email", isEqualTo: email)
.get();
if (emailRef.docs.isNotEmpty) {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text(
"Tento email patří jinému uživateli, prosím jiný email."),
),
);
return;
}
FirebaseFirestore.instance.collection("users").add({
"email": email,
"name": jmeno,
"isAdmin": isAdmin,
"favourite": "Dart",
"username": username
}).then((value) {
Navigator.of(context, rootNavigator: true)
.pop("dialog");
showDialog(
context: context,
builder: (_) => const AlertDialog(
scrollable: true,
content: Text("Uživatel vytvořen")),
);
});
}
},
child: const Text("Přidat"),
),
const SizedBox(height: 20),
const Text(
"Uživatel si při prvním přihlášení vytvoří heslo sám",
textAlign: TextAlign.center,
),
],
),
),
);
},
),
),
);
}
showUpravitUsername(id, oldUsername) async {
GlobalKey<FormState> key = GlobalKey<FormState>();
String newUsername = oldUsername;
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Username", style: Vzhled.velkyText),
scrollable: true,
content: SizedBox(
width: 50.w,
child: Form(
key: key,
child: Column(
children: [
TextFormField(
initialValue: oldUsername,
decoration: Vzhled.inputDecoration("Nové username"),
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
}
return null;
},
onChanged: (value) {
newUsername = value;
},
),
const SizedBox(height: 15),
OutlinedButton(
style: Vzhled.orangeCudlik,
onPressed: () async {
if (key.currentState!.validate()) {
// kontrola ci niekto nevyuziva username
var usernameRef = await FirebaseFirestore.instance
.collection("users")
.where("username", isEqualTo: newUsername)
.get();
if (newUsername.isEmpty) {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text("Toto pole je povinné!"),
),
);
return;
}
if (usernameRef.docs.isNotEmpty &&
oldUsername != newUsername) {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (c) => const AlertDialog(
title: Text("Chyba"),
content: Text(
"Toto username patří jinému uživateli, prosím napište nové username."),
),
);
return;
}
FirebaseFirestore.instance
.collection("users")
.doc(id)
.update({"username": newUsername}).then((value) =>
Navigator.of(context, rootNavigator: true)
.pop("dialog"));
}
},
child: const Text("Uložit"))
],
),
),
),
),
);
}
showPrava(id, isAdmin) async {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Práva uživatele", style: Vzhled.velkyText),
scrollable: true,
content: SizedBox(
width: 20.w,
child: DropdownButton(
value: isAdmin,
items: ["Admin", "Uživatel"]
.map((e) => DropdownMenuItem(
value: e == "Admin" ? true : false, child: Text(e)))
.toList(),
onChanged: (value) {
FirebaseFirestore.instance
.collection("users")
.doc(id)
.update({"isAdmin": value});
Navigator.of(context, rootNavigator: true).pop("dialog");
},
)),
),
);
}
showOdstranit(id) async {
showReallyDelete(context, () {
FirebaseFirestore.instance.collection("users").doc(id).delete();
Navigator.of(context, rootNavigator: true).pop("dialog");
}, doNavigatorPop: false);
}
}

View file

@ -1,152 +0,0 @@
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:csv/csv.dart';
import 'package:denikprogramatora/utils/datum_cas.dart';
import 'package:denikprogramatora/utils/loading_widget.dart';
import 'package:file_picker/file_picker.dart';
import 'package:firebase_auth/firebase_auth.dart';
final csvHeader = "id,date,time_spent,language,rating,description".split(",");
Future<int> importCsv(String name) async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
dialogTitle: "Vyberte CSV soubor s databází",
type: FileType.custom,
allowedExtensions: ['csv']);
if (result == null) return -1;
var content = const CsvToListConverter(eol: "\n").convert(
String.fromCharCodes(
result.files.first.bytes!)); // načíst a rozdělit na řádky
// Zkontrolovat počet sloupců
if (content.first.length != csvHeader.length) {
throw "CSV soubor není v platném formátu: neplatný počet sloupců";
}
Map<String, List<dynamic>> csv = {};
var nazvySloupcu = [];
for (var radek in content) {
if (nazvySloupcu.isEmpty) {
// získat názvy sloupců
nazvySloupcu = radek;
if (nazvySloupcu.any((element) => !csvHeader.contains(element))) {
throw "CSV soubor není v platném formátu: neznámý název sloupce";
}
for (var nazevSloupce in nazvySloupcu) {
csv[nazevSloupce] = [];
}
continue;
}
if (radek.length != nazvySloupcu.length) continue;
// zkontrolovat, že máme všechny potřebné údaje v řádku
var j = 0;
var neplatny = false;
for (var sloupec in radek) {
if ((nazvySloupcu[j] != "description") && sloupec == "") {
neplatny = true;
break;
}
j++;
}
if (neplatny) continue;
// přiřadit hodnotu sloupce do mapy
var i = 0;
for (var sloupec in radek) {
csv[nazvySloupcu[i]]!.add(sloupec);
i++;
}
}
var userDoc = await FirebaseFirestore.instance
.collection("users/${FirebaseAuth.instance.currentUser!.uid}/records")
.get();
var p = 0;
var pouzitaId = <String>[];
for (var i = 0; i < csv[nazvySloupcu[0]]!.length; i++) {
var id = csv[nazvySloupcu[nazvySloupcu.indexOf("id")]]![i];
if (pouzitaId.contains(id)) continue;
pouzitaId.add(id);
if (userDoc.docs.any((element) => element.id == id)) {
continue;
} // přeskočit dokumenty se stejným ID
var date = csv[nazvySloupcu[nazvySloupcu.indexOf("date")]]![i].split("-");
var timeToSec = textToSec(
csv[nazvySloupcu[nazvySloupcu.indexOf("time_spent")]]![i] as String);
if (timeToSec == null) continue;
var timeSpent = Duration(seconds: timeToSec);
var lang = csv[nazvySloupcu[nazvySloupcu.indexOf("language")]]![i];
Map<String, dynamic> jazyk = {};
if (jazyky.any((element) => element["jazyk"] == lang)) {
jazyk = jazyky.where((element) => element["jazyk"] == lang).toList()[0];
} else {
jazyk = {"jazyk": lang, "barva": 0xffffffff};
}
var rating = csv[nazvySloupcu[nazvySloupcu.indexOf("rating")]]![i];
if (rating > 5 || rating < 0) rating = 0;
var desc = csv[nazvySloupcu[nazvySloupcu.indexOf("description")]]![i];
DateTime? d;
try {
d = DateTime.parse(
"${date[2]}-${int.parse(date[1]) < 10 ? '0${date[1]}' : date[1]}-${int.parse(date[0]) < 10 ? "0${date[0]}" : date[0]}");
} catch (e) {
continue;
}
await FirebaseFirestore.instance
.collection("users/${FirebaseAuth.instance.currentUser!.uid}/records")
.doc(id)
.set({
"date": d,
"time_spent": timeSpent.durationText,
"time_spentRaw": timeSpent.inSeconds,
"programming_language": jazyk,
"rating": rating,
"descriptionRaw": desc,
"description": null,
"programmer": FirebaseAuth.instance.currentUser!.uid,
"categories": []
});
p++;
}
return p;
}
Future<List<int>> exportCsv() async {
List<List<dynamic>> csv = [csvHeader];
var records = await FirebaseFirestore.instance
.collection("users/${FirebaseAuth.instance.currentUser!.uid}/records")
.get();
for (var rec in records.docs) {
var data = rec.data();
csv.add([
rec.id,
(data['date'] as Timestamp).toDate().rawDateString,
Duration(seconds: data['time_spentRaw']).durationText,
data['programming_language']['jazyk'],
data['rating'],
data['descriptionRaw']
]);
}
return utf8.encode(const ListToCsvConverter(eol: "\n").convert(csv));
}
/// Převede `40 hodin 50 minut` na sekundy
int? textToSec(String vstup) {
var match =
RegExp(r'(\d+) hodin(?: |y |a )(\d+) minut(?:$|a$|y$)').firstMatch(vstup);
if (match == null) return null;
print(match.group(1));
print(match.group(2));
var h = int.tryParse(match.group(1)!);
var m = int.tryParse(match.group(2)!);
if (h == null || m == null) return null;
return h * 3600 + m * 60;
}

5
lib/utils/datum.dart Normal file
View file

@ -0,0 +1,5 @@
extension DateString on DateTime {
String get dateString => "$day. $month. $year";
String get dateTimeString =>
"$day. $month. $year $hour:${minute < 10 ? "0$minute" : minute}";
}

View file

@ -1,11 +0,0 @@
extension DateString on DateTime {
String get dateString => "$day. $month. $year";
String get dateTimeString =>
"$day. $month. $year $hour:${minute < 10 ? "0$minute" : minute}";
String get rawDateString => "$day-$month-$year";
}
extension TextDuration on Duration {
String get durationText =>
"$inHours hodin${(inHours < 5 && inHours > 1) ? "y" : (inHours == 1) ? "a" : ""} ${inMinutes - inHours * 60} minut${(inMinutes - inHours * 60) < 5 && (inMinutes - inHours * 60) > 1 ? "y" : (inMinutes - inHours * 60) == 1 ? "a" : ""}";
}

View file

@ -18,7 +18,7 @@ final jazyky = <Map<String, dynamic>>[
{"jazyk": "C#", "barva": 0xff8200f3},
{"jazyk": "JavaScript", "barva": 0xfffdd700},
{"jazyk": "Python", "barva": 0xff0080ee},
{"jazyk": "PHP", "barva": 0xff00abff},
{"jazyk": "PHP🤢", "barva": 0xff00abff},
{"jazyk": "C++", "barva": 0xff1626ff},
{"jazyk": "Kotlin", "barva": 0xffe34b7c},
{"jazyk": "Java", "barva": 0xfff58219},

View file

@ -1,12 +1,12 @@
import 'package:denikprogramatora/okna/app.dart';
import 'package:denikprogramatora/utils/datum_cas.dart';
import 'package:denikprogramatora/utils/datum.dart';
import 'package:denikprogramatora/utils/devicecontainer.dart';
import 'package:denikprogramatora/utils/dokument.dart';
import 'package:denikprogramatora/utils/loading_widget.dart';
import 'package:denikprogramatora/utils/programmer.dart';
import 'package:denikprogramatora/utils/really_delete.dart';
import 'package:denikprogramatora/utils/vzhled.dart';
import 'package:duration_picker/duration_picker.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:fleather/fleather.dart';
import 'package:flutter/material.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
@ -15,33 +15,37 @@ import 'package:uuid/uuid.dart';
import '../utils/my_category.dart';
Future<void> showCreateItemDialog(context,
{DateTime? originDate,
Duration? timeSpent,
{DateTime? from,
DateTime? to,
String? j,
int hvezdicky = 0,
String? p,
List<MyCategory>? k,
String? originalId,
ParchmentDocument? doc}) async {
DateTime date = originDate ??
DateTime fromDate = from ??
DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day,
DateTime.now().hour - 1);
DateTime toDate = to ??
DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day,
DateTime.now().hour);
// nastavení jazyka na oblíbený jazyk
String jazyk = "C#";
if (j == null) {
await ref.get().then((value) {
jazyk = value["favourite"] ?? "Dart";
jazyk = value["favourite"];
});
} else {
jazyk = j;
}
timeSpent = timeSpent ?? const Duration(hours: 0, minutes: 0);
int review = hvezdicky;
Programmer programmer = Programmer(name, "pK8iCAtMFiUUhK9FJd6HpWdwA3I3");
Programmer programmer = p == null
? Programmer(name, userUid)
: await Programmer.ziskatProgramatora(
FirebaseAuth.instance.currentUser!, p);
List<MyCategory> categories = k ?? [];
@ -79,25 +83,39 @@ Future<void> showCreateItemDialog(context,
const SizedBox(height: 15),
Row(
children: [
Text(
"Datum ${date.day}. ${date.month}. ${date.year}"),
Text("Od ${toDate.dateTimeString}"),
const SizedBox(width: 15),
TextButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: date,
initialDate: fromDate,
firstDate:
DateTime(DateTime.now().year - 5),
lastDate:
DateTime(DateTime.now().year + 5))
.then((value) {
showTimePicker(
context: context,
initialTime:
TimeOfDay.fromDateTime(fromDate))
.then((time) {
if (value!.day == toDate.day &&
value.month == toDate.month &&
value.year == toDate.year &&
(time!.hour > toDate.hour ||
(time.hour <= toDate.hour &&
time.minute > toDate.minute))) {
return;
}
setState(() {
date = DateTime(
value!.year,
fromDate = DateTime(
value.year,
value.month,
value.day,
);
time!.hour,
time.minute);
});
});
});
},
@ -108,44 +126,88 @@ Future<void> showCreateItemDialog(context,
)
],
),
const SizedBox(height: 5),
Row(
children: [
Text("Do ${toDate.dateTimeString}"),
const SizedBox(width: 15),
TextButton(
onPressed: () {
showDatePicker(
context: context,
initialDate: toDate,
firstDate:
DateTime(DateTime.now().year - 5),
lastDate:
DateTime(DateTime.now().year + 5))
.then((value) {
showTimePicker(
context: context,
initialTime:
TimeOfDay.fromDateTime(toDate))
.then((time) {
if (value!.day == fromDate.day &&
value.month == fromDate.month &&
value.year == fromDate.year &&
(time!.hour < fromDate.hour ||
(time.hour >= fromDate.hour &&
time.minute <
fromDate.minute))) {
return;
}
setState(() {
toDate = DateTime(value.year, value.month,
value.day, time!.hour, time.minute);
});
});
});
},
child: const Text(
("Změnit"),
style: Vzhled.textBtn,
),
)
],
),
const SizedBox(height: 30),
Row(
children: [
Text(
"Strávený čas ${timeSpent!.inHours} hodin ${timeSpent!.inMinutes - timeSpent!.inHours * 60} minut"),
const SizedBox(width: 15),
TextButton(
const Align(
alignment: Alignment.centerLeft,
child: Text(
"Strávený čas",
style: Vzhled.nadpis,
),
),
const SizedBox(height: 15),
Align(
alignment: Alignment.centerLeft,
child: Text(
"${toDate.difference(fromDate).inHours} ${toDate.difference(fromDate).inHours == 1 ? "hodina" : toDate.difference(fromDate).inHours > 1 && toDate.difference(fromDate).inHours < 5 ? "hodiny" : "hodin"}${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60 == 0) ? "" : " a ${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60)} ${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) == 1 ? "minuta" : (toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) > 1 && (toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) < 5 ? "minuty" : "minut"}"}"),
),
const SizedBox(height: 30),
const Align(
alignment: Alignment.centerLeft,
child: Text(
"Programátor",
style: Vzhled.nadpis,
),
),
const SizedBox(height: 15),
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () async {
await showDurationPicker(
context: context,
baseUnit: BaseUnit.hour,
initialTime:
timeSpent ?? const Duration())
.then((hours) {
showDurationPicker(
context: context,
baseUnit: BaseUnit.minute,
initialTime: Duration(
minutes: (timeSpent ??
const Duration())
.inMinutes -
(timeSpent ??
const Duration())
.inHours *
60))
.then((minutes) => setState(() {
timeSpent = Duration(
hours: hours!.inHours,
minutes: minutes!.inMinutes);
}));
await showProgrammersDialog(context, doc: doc)
.then((value) {
setState(() {
programmer = value;
});
});
},
child: const Text(
"Změnit",
child: Text(
programmer.name,
style: Vzhled.textBtn,
),
)
],
),
),
],
),
@ -288,33 +350,35 @@ Future<void> showCreateItemDialog(context,
onPressed: () {
if (originalId == null) {
ref.collection("records").add({
"date": DateTime(date.year, date.month, date.day),
"time_spent": timeSpent!.durationText,
"time_spentRaw": timeSpent!.inSeconds,
"fromDate": fromDate,
"toDate": toDate,
"codingTime":
"${toDate.difference(fromDate).inHours} ${toDate.difference(fromDate).inHours == 1 ? "hodina" : toDate.difference(fromDate).inHours > 1 && toDate.difference(fromDate).inHours < 5 ? "hodiny" : "hodin"}${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60 == 0) ? "" : " a ${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60)} ${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) == 1 ? "minuta" : (toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) > 1 && (toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) < 5 ? "minuty" : "minut"}"}",
"programmer": programmer.id,
"programming_language": jazyky
"programmerName": programmer.name,
"language": jazyky
.where((element) => element["jazyk"] == jazyk)
.toList()[0],
"rating": review,
"review": review,
"categories": categories.map((e) => e.id).toList(),
"description": controller.document.toActualJson(),
"descriptionRaw": controller.document.toPlainText()
}).then((value) =>
Navigator.of(context, rootNavigator: true)
.pop("dialog"));
return;
}
ref.collection("records").doc(originalId).update({
"date": DateTime(date.year, date.month, date.day),
"time_spentRaw": timeSpent!.inSeconds,
"time_spent": timeSpent!.durationText,
"fromDate": fromDate,
"toDate": toDate,
"codingTime":
"${toDate.difference(fromDate).inHours} ${toDate.difference(fromDate).inHours == 1 ? "hodina" : toDate.difference(fromDate).inHours > 1 && toDate.difference(fromDate).inHours < 5 ? "hodiny" : "hodin"}${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60 == 0) ? "" : " a ${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60)} ${(toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) == 1 ? "minuta" : (toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) > 1 && (toDate.difference(fromDate).inMinutes - toDate.difference(fromDate).inHours * 60) < 5 ? "minuty" : "minut"}"}",
"programmer": programmer.id,
"programming_language": jazyky
"programmerName": programmer.name,
"language": jazyky
.where((element) => element["jazyk"] == jazyk)
.toList()[0],
"rating": review,
"review": review,
"categories": categories.map((e) => e.id).toList(),
"descriptionRaw": controller.document.toPlainText(),
"description": controller.document.toActualJson(),
}).then((value) =>
Navigator.of(context, rootNavigator: true).pop("dialog"));
@ -334,6 +398,277 @@ Future<void> showCreateItemDialog(context,
);
}
Future<Programmer> showProgrammersDialog(context,
{ParchmentDocument? doc, bool jenMenit = false}) async {
bool showAddProgrammer = false;
bool editing = false;
GlobalKey<FormState> key = GlobalKey<FormState>();
TextEditingController nameCon = TextEditingController();
late String editId;
Programmer programmer = Programmer(name, userUid);
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Programátoři", style: Vzhled.velkyText),
scrollable: true,
content: SizedBox(
width: 50.w,
child: StatefulBuilder(
builder: (context, setState) {
return showAddProgrammer
? Form(
key: key,
child: Column(
children: [
Row(
children: [
Expanded(
child: TextFormField(
controller: nameCon,
decoration: Vzhled.inputDecoration("Jméno"),
validator: (value) {
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
}
return null;
},
),
),
const SizedBox(width: 5),
TextButton(
onPressed: () {
if (key.currentState!.validate()) {
if (editing) {
ref
.collection("programmers")
.doc(editId)
.update({"name": nameCon.text});
} else {
var uuid = const Uuid();
String id = uuid.v1();
ref
.collection("programmers")
.doc(id)
.set({"name": nameCon.text, "id": id});
}
nameCon.text = "";
setState(() {
editing = false;
showAddProgrammer = false;
});
}
},
child: Text(
editing ? "Uložit" : "Přidat",
style: Vzhled.textBtn,
),
),
],
),
const SizedBox(height: 5),
TextButton(
onPressed: () {
setState(() {
showAddProgrammer = false;
});
},
child: const Text(
"Zpátky",
style: Vzhled.textBtn,
),
),
],
),
)
: Column(
children: [
Material(
color: Vzhled.dialogColor,
child: InkWell(
splashColor: (jenMenit) ? Colors.transparent : null,
onTap: () {
if (jenMenit) return;
programmer = Programmer(name, userUid);
Navigator.of(context, rootNavigator: true)
.pop("dialog");
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(name),
if (!jenMenit)
TextButton(
onPressed: () {
programmer = Programmer(name, userUid);
Navigator.of(context, rootNavigator: true)
.pop("dialog");
},
child: const Text(
"Vybrat",
style: Vzhled.textBtn,
),
),
],
),
),
),
),
StreamBuilder(
stream: ref.collection("programmers").snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var docs = snapshot.data!.docs;
return Column(
children: List.generate(docs.length, (index) {
var data = docs[index].data();
return Material(
color: Vzhled.dialogColor,
child: InkWell(
splashColor: (jenMenit)
? Colors.transparent
: null,
onTap: () {
if (jenMenit) return;
programmer = Programmer(
data["name"], data["id"]);
Navigator.of(context,
rootNavigator: true)
.pop("dialog");
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(data["name"]),
Row(
children: [
if (!jenMenit)
TextButton(
onPressed: () {
programmer = Programmer(
data["name"],
data["id"]);
Navigator.of(context,
rootNavigator:
true)
.pop("dialog");
},
child: const Text(
"Vybrat",
style: Vzhled.textBtn,
),
),
IconButton(
onPressed: () {
editId = data["id"];
setState(() {
showAddProgrammer = true;
editing = true;
nameCon =
TextEditingController(
text:
data["name"]);
});
},
icon: const Icon(Icons.edit,
color: Vzhled.textColor),
),
IconButton(
onPressed: () {
showReallyDelete(context,
() async {
Navigator.of(context,
rootNavigator:
true)
.pop("dialog");
Navigator.of(context,
rootNavigator:
true)
.pop("dialog");
if (doc != null) {
Navigator.of(context,
rootNavigator:
true)
.pop("dialog");
}
// deleting all records
await ref
.collection("records")
.where("programmer",
isEqualTo:
data["id"])
.get()
.then((value) {
for (var snap
in value.docs) {
ref
.collection(
"records")
.doc(snap.id)
.delete();
}
});
// deleting
await ref
.collection(
"programmers")
.doc(data["id"])
.delete();
},
doNavigatorPop: false,
text:
"Odstranit programátora a všechny jeho záznamy?");
},
icon: const Icon(Icons.delete,
color: Vzhled.textColor),
),
],
)
],
),
),
),
);
}),
);
}
return const LoadingWidget();
}),
const SizedBox(height: 5),
TextButton(
onPressed: () {
setState(() {
showAddProgrammer = true;
});
},
child: const Text(
"Přidat nového programátora",
style: Vzhled.textBtn,
),
),
],
);
},
),
),
),
);
return programmer;
}
Future<List<MyCategory>> showCategoriesDialog(
context, List<MyCategory> categories,
{bool jenMenit = false}) async {

View file

@ -4,29 +4,29 @@ var razeni = [
// VZESTUPNĚ ČAS
(QueryDocumentSnapshot<Map<String, dynamic>> a,
QueryDocumentSnapshot<Map<String, dynamic>> b) =>
((a.data()["date"] as Timestamp).toDate().hour ==
(b.data()["date"] as Timestamp).toDate().hour)
? (a.data()["date"] as Timestamp)
((a.data()["fromDate"] as Timestamp).toDate().hour ==
(b.data()["fromDate"] as Timestamp).toDate().hour)
? (a.data()["fromDate"] as Timestamp)
.toDate()
.minute
.compareTo((b.data()["date"] as Timestamp).toDate().minute)
: (a.data()["date"] as Timestamp)
.compareTo((b.data()["fromDate"] as Timestamp).toDate().minute)
: (a.data()["fromDate"] as Timestamp)
.toDate()
.hour
.compareTo((b.data()["date"] as Timestamp).toDate().hour),
.compareTo((b.data()["fromDate"] as Timestamp).toDate().hour),
// SESTUPNĚ ČAS
(QueryDocumentSnapshot<Map<String, dynamic>> a,
QueryDocumentSnapshot<Map<String, dynamic>> b) =>
((b.data()["date"] as Timestamp).toDate().hour ==
(a.data()["date"] as Timestamp).toDate().hour)
? (b.data()["date"] as Timestamp)
((b.data()["fromDate"] as Timestamp).toDate().hour ==
(a.data()["fromDate"] as Timestamp).toDate().hour)
? (b.data()["fromDate"] as Timestamp)
.toDate()
.minute
.compareTo((a.data()["date"] as Timestamp).toDate().minute)
: (b.data()["date"] as Timestamp)
.compareTo((a.data()["fromDate"] as Timestamp).toDate().minute)
: (b.data()["fromDate"] as Timestamp)
.toDate()
.hour
.compareTo((a.data()["date"] as Timestamp).toDate().hour),
.compareTo((a.data()["fromDate"] as Timestamp).toDate().hour),
// VZESTUPNĚ HODNOCENÍ
(QueryDocumentSnapshot<Map<String, dynamic>> a,
QueryDocumentSnapshot<Map<String, dynamic>> b) =>

View file

@ -1,6 +1,6 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:denikprogramatora/okna/app.dart';
import 'package:denikprogramatora/utils/datum_cas.dart';
import 'package:denikprogramatora/utils/datum.dart';
import 'package:denikprogramatora/utils/devicecontainer.dart';
import 'package:denikprogramatora/utils/my_category.dart';
import 'package:denikprogramatora/utils/new_record_dialog.dart';
@ -12,11 +12,12 @@ import 'package:flutter/material.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
void showInfoDialog(
context, Map<String, dynamic> data, String originalId, String jmeno) async {
var date = (data["date"] as Timestamp).toDate().toLocal();
context, Map<String, dynamic> data, String originalId) async {
var denOd = (data["fromDate"] as Timestamp).toDate().toLocal();
var denDo = (data["toDate"] as Timestamp).toDate().toLocal();
List<MyCategory> categories = [];
if (((data["categories"] ?? []) as List).isNotEmpty) {
if ((data["categories"] as List).isNotEmpty) {
for (var category in data["categories"]) {
await ref.collection("categories").doc(category).get().then((value) {
var data = value.data();
@ -29,9 +30,7 @@ void showInfoDialog(
showDialog(
context: context,
builder: (_) {
var document = (data["description"] != null && data["description"] != "")
? ParchmentDocument.fromJson(data["description"])
: (ParchmentDocument()..insert(0, data["descriptionRaw"]));
var document = ParchmentDocument.fromJson(data["description"]);
var controller = FleatherController(document);
return AlertDialog(
@ -52,13 +51,13 @@ void showInfoDialog(
style: Vzhled.purpleCudlik,
onPressed: () {
showCreateItemDialog(context,
originDate: date,
timeSpent: Duration(seconds: data["time_spentRaw"]),
from: denOd,
to: denDo,
k: categories,
p: data["programmer"],
hvezdicky: data["rating"],
hvezdicky: data["review"],
originalId: originalId,
j: data["programming_language"]["jazyk"],
j: data["language"]["jazyk"],
doc: document)
.then((_) => Navigator.of(context).pop());
},
@ -70,7 +69,7 @@ void showInfoDialog(
),
],
title: Text(
"Záznam ze dne ${date.dateString}",
"Záznam ze dne ${denOd.dateString}",
style: Vzhled.dialogNadpis,
),
scrollable: true,
@ -98,7 +97,9 @@ void showInfoDialog(
style: Vzhled.nadpis,
),
Text(
"${date.dateString} (${data["time_spent"]})",
(Device.screenType == ScreenType.mobile)
? "od ${denOd.dateTimeString}\ndo ${denDo.dateTimeString} (${data["codingTime"]})"
: "od ${denOd.dateTimeString} do ${denDo.dateTimeString} (${data["codingTime"]})",
)
],
),
@ -112,10 +113,9 @@ void showInfoDialog(
style: Vzhled.nadpis,
),
Text(
data["programming_language"]["jazyk"],
data["language"]["jazyk"],
style: TextStyle(
color: Color(
data["programming_language"]["barva"])),
color: Color(data["language"]["barva"])),
)
],
),
@ -138,9 +138,7 @@ void showInfoDialog(
style: Vzhled.nadpis,
),
Text(
FirebaseAuth
.instance.currentUser!.displayName ??
jmeno,
data["programmerName"],
)
],
),
@ -158,7 +156,7 @@ void showInfoDialog(
5,
(index) {
return Icon(Icons.star,
color: (index + 1) <= data["rating"]
color: (index + 1) <= data["review"]
? Colors.yellow
: Colors.grey);
},

View file

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "64fcb0dbca4386356386c085142fa6e79c00a3326ceaa778a2d25f5d9ba61441"
sha256: "6215ac7d00ed98300b72f45ed2b38c2ca841f9f4e6965fab33cbd591e45e4473"
url: "https://pub.dev"
source: hosted
version: "1.0.16"
version: "1.0.13"
async:
dependency: transitive
description:
@ -45,26 +45,26 @@ packages:
dependency: "direct main"
description:
name: cloud_firestore
sha256: "65f148d9f5b4f389320abb45847120cf5e46094c1a8cbc64934ffc1e29688596"
sha256: "9e775f9df26a165444bd5240f70bfee6f11b35c5e913e93ed4b06bf50b231325"
url: "https://pub.dev"
source: hosted
version: "4.4.3"
version: "4.3.2"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
sha256: "43ccae09f7e0c82752e69c251c6dc5efcdff4ddcfc09564175a28657bbd74188"
sha256: ab35c068896ff769ce7e8de8198228d512e7f056fc8f26b2ff53ea3f97c8545f
url: "https://pub.dev"
source: hosted
version: "5.11.3"
version: "5.10.2"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
sha256: e054c007217e28e07179bbae0564c2a4f6338a60bddb0c139e4834e953f4b95c
sha256: b7b52c2ad50d1105f2e0585a34288da415cf9d1037470985c7c57cce7b06d95f
url: "https://pub.dev"
source: hosted
version: "3.3.3"
version: "3.2.2"
collection:
dependency: transitive
description:
@ -89,14 +89,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.17.2"
csv:
cupertino_icons:
dependency: "direct main"
description:
name: csv
sha256: "18aef53ab72181a0b5384562d18c8cbd57e941e24cb8e54eb41409d3d8abdc6d"
name: cupertino_icons
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
url: "https://pub.dev"
source: hosted
version: "5.0.1"
version: "1.0.5"
diff_match_patch:
dependency: transitive
description:
@ -105,14 +105,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.1"
duration_picker:
dependency: "direct main"
description:
name: duration_picker
sha256: "052b34dac04c29f3849bb3817a26c5aebe9e5f0697c3a374be87db2b84d75753"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
fake_async:
dependency: transitive
description:
@ -121,54 +113,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
url: "https://pub.dev"
source: hosted
version: "2.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9
url: "https://pub.dev"
source: hosted
version: "5.2.5"
firebase_auth:
dependency: "direct main"
description:
name: firebase_auth
sha256: "9907d80446466e638dad31c195150b305dffd145dc57610fcd12c72289432143"
sha256: "843e307e9b7faa026dd9970e584b5d53265fb5a0c4323883fecdce89ec05d56a"
url: "https://pub.dev"
source: hosted
version: "4.2.9"
version: "4.2.6"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
sha256: c645fec50b0391aa878288f58fa4fe9762c271380c457aedf5c7c9b718604f68
sha256: "8702baa08ad5aa6daa023082d612ca168bf3f7de81e3d56e1df18321f76d675f"
url: "https://pub.dev"
source: hosted
version: "6.11.11"
version: "6.11.8"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
sha256: "2dcf2a36852b9091741b4a4047a02e1f2c43a62c6cacec7df573a793a6543e6d"
sha256: "0c01b9c772ee730df03ac92102e538873558f908d6e42602f6ff9c61dead8d58"
url: "https://pub.dev"
source: hosted
version: "5.2.8"
version: "5.2.5"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: fe30ac230f12f8836bb97e6e09197340d3c584526825b1746ea362a82e1e43f7
sha256: be13e431c0c950f0fc66bdb67b41b8059121d7e7d8bbbc21fb59164892d561f8
url: "https://pub.dev"
source: hosted
version: "2.7.0"
version: "2.5.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -181,10 +157,10 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "291fbcace608aca6c860652e1358ef89752be8cc3ef227f8bbcd1e62775b833a"
sha256: "4b3a41410f3313bb95fd560aa5eb761b6ad65c185de772c72231e8b4aeed6d18"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.1.1"
fleather:
dependency: "direct main"
description:
@ -211,14 +187,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "4bef634684b2c7f3468c77c766c831229af829a0cd2d4ee6c1b99558bd14e5d2"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
flutter_test:
dependency: "direct dev"
description: flutter
@ -321,10 +289,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.3"
quill_delta:
dependency: transitive
description:
@ -414,66 +382,66 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b
url: "https://pub.dev"
source: hosted
version: "6.1.10"
version: "6.1.9"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "1f4d9ebe86f333c15d318f81dcdc08b01d45da44af74552608455ebdc08d9732"
sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1"
url: "https://pub.dev"
source: hosted
version: "6.0.24"
version: "6.0.23"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: c9cd648d2f7ab56968e049d4e9116f96a85517f1dd806b96a86ea1018a3a82e5
sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815"
url: "https://pub.dev"
source: hosted
version: "6.1.1"
version: "6.1.0"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: e29039160ab3730e42f3d811dc2a6d5f2864b90a70fb765ea60144b03307f682
sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "2dddb3291a57b074dade66b5e07e64401dd2487caefd4e9e2f467138d8c7eb06"
sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370"
sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.1"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "574cfbe2390666003c3a1d129bdc4574aaa6728f0c00a4829a81c316de69dd9b"
sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0"
url: "https://pub.dev"
source: hosted
version: "2.0.15"
version: "2.0.14"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "97c9067950a0d09cbd93e2e3f0383d1403989362b97102fbf446473a48079a4b"
sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615
url: "https://pub.dev"
source: hosted
version: "3.0.4"
version: "3.0.3"
uuid:
dependency: "direct main"
description:
@ -490,14 +458,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
win32:
dependency: transitive
description:
name: win32
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=2.18.5 <3.0.0"
flutter: ">=3.3.0"

View file

@ -37,6 +37,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
firebase_core: ^2.4.0
responsive_sizer: ^3.1.1
firebase_auth: ^4.2.1
@ -45,9 +46,6 @@ dependencies:
multi_select_flutter: ^4.1.3
fleather: ^1.4.0
url_launcher: ^6.1.8
file_picker: ^5.2.5
csv: ^5.0.1
duration_picker: ^1.1.1
dev_dependencies:
flutter_test:

View file

@ -1,6 +1,6 @@
{
"name": "Kodelog",
"short_name": "Kodelog",
"name": "denikprogramatora",
"short_name": "denikprogramatora",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",