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 FROM passsy/flutterw:base-latest
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
COPY . . COPY . .
RUN pacman --noconfirm -S nodejs npm git base-devel unzip
RUN npm i -g pnpm
RUN ./flutterw config --no-analytics RUN ./flutterw config --no-analytics
RUN cd api && pnpm i && pnpm run build ENTRYPOINT ./flutterw run --release --web-port=80 --web-hostname 0.0.0.0 -d web-server
ENTRYPOINT PORT=80 node /app/api/build/index.js
EXPOSE 80 EXPOSE 80

View file

@ -19,7 +19,7 @@ Vydáno pod licencí AGPL verze 3
1. Clone 1. Clone
2. Build 2. Build
## Obrázky (verze 1.x) ## Obrázky
![Screenshot přihlašovací obrazovky](images/01.png) ![Screenshot přihlašovací obrazovky](images/01.png)
![Screenshot přihlašovací obrazovky](images/02.png) ![Screenshot přihlašovací obrazovky](images/02.png)
![Screenshot přihlašovací obrazovky](images/03.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:flutter_localizations/flutter_localizations.dart';
import 'package:responsive_sizer/responsive_sizer.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 Copyright (C) 2022 Matyáš Caras a Richard Pavlikán
@ -27,7 +27,8 @@ import 'firebase_options.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions
.currentPlatform, //TODO: Přidejte si vlastní firebase nastavení
); );
runApp(const MyApp()); runApp(const MyApp());
} }
@ -39,7 +40,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ResponsiveSizer( return ResponsiveSizer(
builder: (p0, p1, p2) => MaterialApp( builder: (p0, p1, p2) => MaterialApp(
title: 'Kodelog', title: 'Deník Programátora',
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
scaffoldBackgroundColor: Vzhled.backgroundColor, 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/app.dart';
import 'package:denikprogramatora/okna/settings.dart'; import 'package:denikprogramatora/okna/settings.dart';
import 'package:denikprogramatora/okna/signin_page.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/devicecontainer.dart';
import 'package:denikprogramatora/utils/input_decoration.dart';
import 'package:denikprogramatora/utils/loading_widget.dart'; import 'package:denikprogramatora/utils/loading_widget.dart';
import 'package:denikprogramatora/utils/months.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/my_container.dart';
import 'package:denikprogramatora/utils/new_record_dialog.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/show_info_dialog.dart';
import 'package:denikprogramatora/utils/vzhled.dart'; import 'package:denikprogramatora/utils/vzhled.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:responsive_sizer/responsive_sizer.dart'; import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:url_launcher/url_launcher_string.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 { class AllRecordsPage extends StatefulWidget {
const AllRecordsPage({super.key}); const AllRecordsPage({super.key});
@ -29,7 +49,30 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
int selectedDay = DateTime.now().day; int selectedDay = DateTime.now().day;
int year = DateTime.now().year; 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; 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 @override
void initState() { void initState() {
@ -43,14 +86,29 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
return; return;
} }
ref.get().then((value) { name = FirebaseAuth.instance.currentUser!.displayName!;
setState(() {
name = FirebaseAuth.instance.currentUser!.displayName ?? ref.collection("programmers").get().then((value) {
value[ for (var snap in value.docs) {
"name"]; // fallback když uživatel je vytvořen skrz firebase admin 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]; mesic = months[DateTime.now().month - 1];
setState(() { setState(() {
@ -90,7 +148,7 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
onPressed: () => showAboutDialog( onPressed: () => showAboutDialog(
context: context, context: context,
applicationName: "Kodelog", applicationName: "Kodelog",
applicationVersion: "2.0.1", applicationVersion: "1.1.0",
applicationLegalese: 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: [ children: [
@ -190,19 +248,31 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
Expanded( Expanded(
child: MyContainer( child: MyContainer(
width: 90.w, width: 90.w,
child: Center( child: DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width:
(Device.screenType == ScreenType.mobile)
? 80.w
: 40.w,
child: Column( child: Column(
children: [ children: [
const Text("Filtr",
style: Vzhled.nadpis),
const SizedBox(height: 15),
Row( Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [ children: [
Text( Flexible(
"Záznamy seřazené \nod ${newestToOldest ? "nejnovějších po nejstarší" : "nejstarších po nejnovější"}"), child: Text(
"Záznamy seřazené od ${newestToOldest ? "nejnovějších po nejstarší" : "nejstarších po nejnovější"}"),
),
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
newestToOldest = !newestToOldest; newestToOldest =
!newestToOldest;
}); });
}, },
child: const Text( 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( SizedBox(
width: (Device.screenType == width: 75,
ScreenType.mobile) 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 ? 80.w
: 40.w, : 40.w,
child: StreamBuilder( child: StreamBuilder(
stream: ref stream: ref
.collection("records") .collection("records")
.orderBy("date", .orderBy("fromDate",
descending: newestToOldest) descending: newestToOldest)
.snapshots(), .snapshots(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
var docs = snapshot.data!.docs; 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( return Column(
children: List.generate( children: List.generate(
docs.length, docs.length,
@ -236,8 +658,7 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
return Padding( return Padding(
padding: padding:
const EdgeInsets.all( const EdgeInsets.all(8.0),
8.0),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius:
@ -245,43 +666,31 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
.all( .all(
Radius.circular(4), Radius.circular(4),
), ),
color: Color(data[ color: Color(
"programming_language"] data["language"]
["barva"]), ["barva"]),
), ),
child: Material( child: Material(
color: color: Colors.transparent,
Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => onTap: () =>
showInfoDialog( showInfoDialog(
context, context,
data, data,
docs[index] docs[index].id),
.id,
name),
child: Padding( child: Padding(
padding: padding:
const EdgeInsets const EdgeInsets
.all(8.0), .all(8.0),
child: Row( child: Row(
children: [ children: [
Text((data["date"] Text(
as Timestamp) "${(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}"),
.toDate()
.dateString),
const SizedBox( const SizedBox(
width: 20, width: 20,
), ),
Text( Text(
"${data["programming_language"]["jazyk"]}", " - ${data["language"]["jazyk"]}")
style: const TextStyle(
fontWeight:
FontWeight
.bold),
),
Text(
" - ${data["time_spent"]}")
], ],
), ),
), ),
@ -301,7 +710,6 @@ class _AllRecordsPageState extends State<AllRecordsPage> {
), ),
), ),
), ),
),
], ],
), ),
) )

View file

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

View file

@ -1,21 +1,34 @@
import 'package:denikprogramatora/okna/app.dart'; import 'package:denikprogramatora/okna/app.dart';
import 'package:denikprogramatora/okna/signin_page.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/devicecontainer.dart';
import 'package:denikprogramatora/utils/loading_widget.dart'; import 'package:denikprogramatora/utils/loading_widget.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:responsive_sizer/responsive_sizer.dart'; import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:url_launcher/url_launcher_string.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/my_container.dart';
import '../utils/new_record_dialog.dart'; import '../utils/new_record_dialog.dart';
import '../utils/vzhled.dart'; import '../utils/vzhled.dart';
import 'all_records.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 { class NastaveniOkno extends StatefulWidget {
const NastaveniOkno({super.key}); const NastaveniOkno({super.key});
@ -26,8 +39,6 @@ class NastaveniOkno extends StatefulWidget {
class _NastaveniOknoState extends State<NastaveniOkno> { class _NastaveniOknoState extends State<NastaveniOkno> {
var _loading = true; var _loading = true;
var name = "error"; var name = "error";
bool isAdmin = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -38,16 +49,7 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
(route) => false); (route) => false);
return; return;
} }
name = FirebaseAuth.instance.currentUser!.displayName!;
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"];
});
});
setState(() { setState(() {
_loading = false; _loading = false;
@ -64,11 +66,9 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
height: 100.h, height: 100.h,
child: (_loading) child: (_loading)
? const LoadingWidget() ? const LoadingWidget()
: Column( : Column(children: [
children: [
DeviceContainer( DeviceContainer(
mainAxisAlignmentDesktop: mainAxisAlignmentDesktop: MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
children: [ children: [
MyContainer( MyContainer(
width: (Device.screenType == ScreenType.mobile) width: (Device.screenType == ScreenType.mobile)
@ -82,7 +82,7 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
onPressed: () => showAboutDialog( onPressed: () => showAboutDialog(
context: context, context: context,
applicationName: "Kodelog", applicationName: "Kodelog",
applicationVersion: "2.0.1", applicationVersion: "1.1.0",
applicationLegalese: 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: [ children: [
@ -160,8 +160,7 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
OutlinedButton( OutlinedButton(
onPressed: () => onPressed: () => showCreateItemDialog(context),
showCreateItemDialog(context),
style: Vzhled.orangeCudlik, style: Vzhled.orangeCudlik,
child: const Text( child: const Text(
"Přidat záznam", "Přidat záznam",
@ -186,18 +185,19 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
10, 10,
), ),
), ),
color: Vzhled.purple), color: Vzhled.purple,
),
width: 400, width: 400,
child: InkWell( child: InkWell(
onTap: () => showEditJazyk(), onTap: () => showProgrammersDialog(context,
jenMenit: true),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.center,
MainAxisAlignment.center,
children: const [ children: const [
Text( Text(
"Oblíbený jazyk", "Upravit programátory",
style: Vzhled.nadpis, style: Vzhled.nadpis,
) )
], ],
@ -223,8 +223,7 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.center,
MainAxisAlignment.center,
children: const [ children: const [
Text( Text(
"Upravit kategorie", "Upravit kategorie",
@ -238,12 +237,6 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
const SizedBox( const SizedBox(
height: 15, height: 15,
), ),
DeviceContainer(
mainAxisAlignmentDesktop:
MainAxisAlignment.center,
mainAxisAlignmentMobile:
MainAxisAlignment.center,
children: [
Container( Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
@ -254,36 +247,14 @@ class _NastaveniOknoState extends State<NastaveniOkno> {
color: Vzhled.purple), color: Vzhled.purple),
width: 400, width: 400,
child: InkWell( child: InkWell(
onTap: () async { onTap: () => showEditJazyk(),
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);
},
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.center,
MainAxisAlignment.center,
children: const [ children: const [
Text( Text(
"Exportovat CSV", "Oblíbený jazyk",
style: Vzhled.nadpis, 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(); return const LoadingWidget();
}), }),
), ),
), ));
);
} }
} }

View file

@ -9,6 +9,23 @@ import 'package:flutter/scheduler.dart';
import 'package:responsive_sizer/responsive_sizer.dart'; import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:url_launcher/url_launcher_string.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 { class SignInPage extends StatefulWidget {
const SignInPage({super.key}); const SignInPage({super.key});
@ -17,10 +34,8 @@ class SignInPage extends StatefulWidget {
} }
class _SignInPageState extends State<SignInPage> { class _SignInPageState extends State<SignInPage> {
bool showSignIn = true;
bool isLoading = true; bool isLoading = true;
bool isSignInWidget = true;
bool showEmailPage = true;
String oldDocId = "";
TextEditingController emailCon = TextEditingController(); TextEditingController emailCon = TextEditingController();
TextEditingController passwordCon = TextEditingController(); TextEditingController passwordCon = TextEditingController();
@ -55,7 +70,7 @@ class _SignInPageState extends State<SignInPage> {
: Stack( : Stack(
children: [ children: [
Center( Center(
child: isSignInWidget ? signInWidget() : registerWidget(), child: showSignIn ? signInWidget() : registerWidget(),
), ),
Positioned( Positioned(
bottom: 10, bottom: 10,
@ -64,7 +79,7 @@ class _SignInPageState extends State<SignInPage> {
onPressed: () => showAboutDialog( onPressed: () => showAboutDialog(
context: context, context: context,
applicationName: "Kodelog", applicationName: "Kodelog",
applicationVersion: "2.0.1", applicationVersion: "1.1.0",
applicationLegalese: 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: [ children: [
@ -106,7 +121,7 @@ class _SignInPageState extends State<SignInPage> {
SizedBox( SizedBox(
width: (Device.screenType == ScreenType.mobile) ? 60.w : 30.w, width: (Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
child: TextFormField( child: TextFormField(
decoration: Vzhled.inputDecoration("E-mail nebo Username"), decoration: Vzhled.inputDecoration("E-mail"),
cursorColor: Vzhled.textColor, cursorColor: Vzhled.textColor,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
autocorrect: false, autocorrect: false,
@ -119,6 +134,9 @@ class _SignInPageState extends State<SignInPage> {
validator: (value) { validator: (value) {
if (value!.trim().isEmpty) { if (value!.trim().isEmpty) {
return "Toto pole je povinné!"; 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; return null;
}, },
@ -170,15 +188,11 @@ class _SignInPageState extends State<SignInPage> {
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
isSignInWidget = false; showSignIn = false;
showEmailPage = true;
}); });
}, },
child: const Text( child: const Text("Registrovat se", style: Vzhled.textBtn),
"Přihlašuji se poprvé", )
style: Vzhled.textBtn,
),
),
], ],
), ),
), ),
@ -187,9 +201,7 @@ class _SignInPageState extends State<SignInPage> {
Widget registerWidget() { Widget registerWidget() {
GlobalKey<FormState> form = GlobalKey<FormState>(); GlobalKey<FormState> form = GlobalKey<FormState>();
return MyContainer( return MyContainer(
height: 70.h,
width: (Device.screenType == ScreenType.mobile) ? 80.w : 40.w, width: (Device.screenType == ScreenType.mobile) ? 80.w : 40.w,
child: Form( child: Form(
key: form, key: form,
@ -204,33 +216,17 @@ class _SignInPageState extends State<SignInPage> {
const SizedBox( const SizedBox(
height: 30, height: 30,
), ),
showEmailPage SizedBox(
? SizedBox( width: (Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
width:
(Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
child: TextFormField( child: TextFormField(
decoration: Vzhled.inputDecoration("Váš e-mail"), decoration: Vzhled.inputDecoration("Jméno"),
cursorColor: Vzhled.textColor, cursorColor: Vzhled.textColor,
keyboardType: TextInputType.emailAddress, controller: nameCon,
autocorrect: false, onFieldSubmitted: (_) {
controller: emailCon, if (form.currentState!.validate()) {
validator: (value) { signUp();
if (value!.trim().isEmpty) {
return "Toto pole je povinné!";
} }
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) { validator: (value) {
if (value!.trim().isEmpty) { if (value!.trim().isEmpty) {
return "Toto pole je povinné!"; return "Toto pole je povinné!";
@ -242,104 +238,97 @@ class _SignInPageState extends State<SignInPage> {
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
showEmailPage SizedBox(
? OutlinedButton( width: (Device.screenType == ScreenType.mobile) ? 60.w : 30.w,
style: Vzhled.orangeCudlik, child: TextFormField(
onPressed: () async { decoration: Vzhled.inputDecoration("E-mail"),
cursorColor: Vzhled.textColor,
controller: emailCon,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
onFieldSubmitted: (_) {
if (form.currentState!.validate()) { if (form.currentState!.validate()) {
var emailRef = await FirebaseFirestore.instance signUp();
.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;
});
} }
}, },
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 { onPressed: () async {
if (form.currentState!.validate()) { if (form.currentState!.validate()) {
await FirebaseAuth.instance signUp();
.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;
});
} }
}, },
style: Vzhled.orangeCudlik, child: const Text("Registrovat se"),
child: const Text("Pokračovat")), ),
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
@ -350,54 +339,21 @@ class _SignInPageState extends State<SignInPage> {
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
isSignInWidget = true; showSignIn = true;
}); });
}, },
child: const Text( child: const Text("Přihlásit se", style: Vzhled.textBtn),
"Účet mám vytvořen, chci se přihlásit", )
style: Vzhled.textBtn,
),
),
], ],
), ),
), ),
); );
} }
void signIn() async { void signIn() {
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"];
}
FirebaseAuth.instance FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: passwordCon.text) .signInWithEmailAndPassword(
email: emailCon.text, password: passwordCon.text)
.then( .then(
(value) => Navigator.pushReplacement( (value) => Navigator.pushReplacement(
context, context,
@ -432,11 +388,49 @@ class _SignInPageState extends State<SignInPage> {
debugPrint(e.toString()); debugPrint(e.toString());
} }
}); });
}
if (mounted) { void signUp() {
setState(() { FirebaseAuth.instance
isLoading = false; .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": "C#", "barva": 0xff8200f3},
{"jazyk": "JavaScript", "barva": 0xfffdd700}, {"jazyk": "JavaScript", "barva": 0xfffdd700},
{"jazyk": "Python", "barva": 0xff0080ee}, {"jazyk": "Python", "barva": 0xff0080ee},
{"jazyk": "PHP", "barva": 0xff00abff}, {"jazyk": "PHP🤢", "barva": 0xff00abff},
{"jazyk": "C++", "barva": 0xff1626ff}, {"jazyk": "C++", "barva": 0xff1626ff},
{"jazyk": "Kotlin", "barva": 0xffe34b7c}, {"jazyk": "Kotlin", "barva": 0xffe34b7c},
{"jazyk": "Java", "barva": 0xfff58219}, {"jazyk": "Java", "barva": 0xfff58219},

View file

@ -1,12 +1,12 @@
import 'package:denikprogramatora/okna/app.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/devicecontainer.dart';
import 'package:denikprogramatora/utils/dokument.dart'; import 'package:denikprogramatora/utils/dokument.dart';
import 'package:denikprogramatora/utils/loading_widget.dart'; import 'package:denikprogramatora/utils/loading_widget.dart';
import 'package:denikprogramatora/utils/programmer.dart'; import 'package:denikprogramatora/utils/programmer.dart';
import 'package:denikprogramatora/utils/really_delete.dart'; import 'package:denikprogramatora/utils/really_delete.dart';
import 'package:denikprogramatora/utils/vzhled.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:fleather/fleather.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:responsive_sizer/responsive_sizer.dart'; import 'package:responsive_sizer/responsive_sizer.dart';
@ -15,33 +15,37 @@ import 'package:uuid/uuid.dart';
import '../utils/my_category.dart'; import '../utils/my_category.dart';
Future<void> showCreateItemDialog(context, Future<void> showCreateItemDialog(context,
{DateTime? originDate, {DateTime? from,
Duration? timeSpent, DateTime? to,
String? j, String? j,
int hvezdicky = 0, int hvezdicky = 0,
String? p, String? p,
List<MyCategory>? k, List<MyCategory>? k,
String? originalId, String? originalId,
ParchmentDocument? doc}) async { ParchmentDocument? doc}) async {
DateTime date = originDate ?? DateTime fromDate = from ??
DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day,
DateTime.now().hour - 1); 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 // nastavení jazyka na oblíbený jazyk
String jazyk = "C#"; String jazyk = "C#";
if (j == null) { if (j == null) {
await ref.get().then((value) { await ref.get().then((value) {
jazyk = value["favourite"] ?? "Dart"; jazyk = value["favourite"];
}); });
} else { } else {
jazyk = j; jazyk = j;
} }
timeSpent = timeSpent ?? const Duration(hours: 0, minutes: 0);
int review = hvezdicky; 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 ?? []; List<MyCategory> categories = k ?? [];
@ -79,25 +83,39 @@ Future<void> showCreateItemDialog(context,
const SizedBox(height: 15), const SizedBox(height: 15),
Row( Row(
children: [ children: [
Text( Text("Od ${toDate.dateTimeString}"),
"Datum ${date.day}. ${date.month}. ${date.year}"),
const SizedBox(width: 15), const SizedBox(width: 15),
TextButton( TextButton(
onPressed: () { onPressed: () {
showDatePicker( showDatePicker(
context: context, context: context,
initialDate: date, initialDate: fromDate,
firstDate: firstDate:
DateTime(DateTime.now().year - 5), DateTime(DateTime.now().year - 5),
lastDate: lastDate:
DateTime(DateTime.now().year + 5)) DateTime(DateTime.now().year + 5))
.then((value) { .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(() { setState(() {
date = DateTime( fromDate = DateTime(
value!.year, value.year,
value.month, value.month,
value.day, 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), const SizedBox(height: 30),
Row( const Align(
children: [ alignment: Alignment.centerLeft,
Text( child: Text(
"Strávený čas ${timeSpent!.inHours} hodin ${timeSpent!.inMinutes - timeSpent!.inHours * 60} minut"), "Strávený čas",
const SizedBox(width: 15), style: Vzhled.nadpis,
TextButton( ),
),
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 { onPressed: () async {
await showDurationPicker( await showProgrammersDialog(context, doc: doc)
context: context, .then((value) {
baseUnit: BaseUnit.hour, setState(() {
initialTime: programmer = value;
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);
}));
}); });
}, },
child: const Text( child: Text(
"Změnit", programmer.name,
style: Vzhled.textBtn, style: Vzhled.textBtn,
), ),
) ),
],
), ),
], ],
), ),
@ -288,33 +350,35 @@ Future<void> showCreateItemDialog(context,
onPressed: () { onPressed: () {
if (originalId == null) { if (originalId == null) {
ref.collection("records").add({ ref.collection("records").add({
"date": DateTime(date.year, date.month, date.day), "fromDate": fromDate,
"time_spent": timeSpent!.durationText, "toDate": toDate,
"time_spentRaw": timeSpent!.inSeconds, "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, "programmer": programmer.id,
"programming_language": jazyky "programmerName": programmer.name,
"language": jazyky
.where((element) => element["jazyk"] == jazyk) .where((element) => element["jazyk"] == jazyk)
.toList()[0], .toList()[0],
"rating": review, "review": review,
"categories": categories.map((e) => e.id).toList(), "categories": categories.map((e) => e.id).toList(),
"description": controller.document.toActualJson(), "description": controller.document.toActualJson(),
"descriptionRaw": controller.document.toPlainText()
}).then((value) => }).then((value) =>
Navigator.of(context, rootNavigator: true) Navigator.of(context, rootNavigator: true)
.pop("dialog")); .pop("dialog"));
return; return;
} }
ref.collection("records").doc(originalId).update({ ref.collection("records").doc(originalId).update({
"date": DateTime(date.year, date.month, date.day), "fromDate": fromDate,
"time_spentRaw": timeSpent!.inSeconds, "toDate": toDate,
"time_spent": timeSpent!.durationText, "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, "programmer": programmer.id,
"programming_language": jazyky "programmerName": programmer.name,
"language": jazyky
.where((element) => element["jazyk"] == jazyk) .where((element) => element["jazyk"] == jazyk)
.toList()[0], .toList()[0],
"rating": review, "review": review,
"categories": categories.map((e) => e.id).toList(), "categories": categories.map((e) => e.id).toList(),
"descriptionRaw": controller.document.toPlainText(),
"description": controller.document.toActualJson(), "description": controller.document.toActualJson(),
}).then((value) => }).then((value) =>
Navigator.of(context, rootNavigator: true).pop("dialog")); 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( Future<List<MyCategory>> showCategoriesDialog(
context, List<MyCategory> categories, context, List<MyCategory> categories,
{bool jenMenit = false}) async { {bool jenMenit = false}) async {

View file

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

View file

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

View file

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

View file

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

View file

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