34 Commits
Author SHA1 Message Date
drake 509bf06128 run 2024-06-13 16:00:28 -05:00
drake 2c85accb7f Fix API Delete 2024-06-13 15:52:01 -05:00
drake cfade0e075 API Rewrite with UI 2024-06-13 15:30:48 -05:00
drake 64e493012a API Rewrite 1 2024-06-13 15:27:13 -05:00
drake 9116876f7b API Rewrite 2024-06-13 14:27:36 -05:00
drake 0a1250dfd2 API Listing half-rewrite 2024-06-12 14:28:13 -05:00
drake 68edb2c3a1 fixes 4 2024-06-06 21:55:56 -05:00
drake ee8b419887 fixes 3 2024-06-06 21:40:28 -05:00
drake d350dde994 fixes 2 2024-06-06 21:31:30 -05:00
drake 00a4965efc fixes 2024-06-06 21:17:31 -05:00
drake 013dc5572b test 2 2024-06-06 21:00:43 -05:00
drake 086f47cab0 test 2024-06-06 20:25:15 -05:00
drake dd0b7460fb fix env vars 2024-06-06 20:10:38 -05:00
drake 6bdb53ecd9 test3 2024-06-06 20:08:26 -05:00
drake 8084532ba1 test2 2024-06-06 20:06:43 -05:00
drake 8efffef7c9 test 2024-06-06 20:05:34 -05:00
drake 9ac5b280d0 fix api address & adjust linux build 2024-06-06 19:48:26 -05:00
drake 96b608d0c1 reformat jenkinsfile again x6 2024-06-06 18:49:18 -05:00
drake bf53db3524 reformat jenkinsfile again x5 2024-06-06 18:32:51 -05:00
drake bdc69a5dc0 reformat jenkinsfile again x4 2024-06-06 18:14:12 -05:00
drake a81cb9ab73 reformat jenkinsfile again x3 2024-06-06 18:12:43 -05:00
drake 5fe4201060 reformat jenkinsfile again x2 2024-06-06 18:06:47 -05:00
drake 355a5b2532 reformat jenkinsfile again 2024-06-06 18:03:17 -05:00
drake abf893cef3 reformat jenkinsfile 2024-06-06 18:00:33 -05:00
drake 339f64ac0d reformat jenkinsfile 2024-06-06 16:41:37 -05:00
jenkins d5e7de8f50 security updates part 2 2024-06-06 15:44:28 -05:00
jenkins e1ab786140 security updates 2024-06-06 15:13:53 -05:00
drake 8f8797fbe6 readme fixes 2024-04-30 03:01:27 +00:00
drake e440438a8a after SLC rubric and README.md 2024-04-29 21:58:55 -05:00
drake 9b8dcda882 Update fbla-api/README.md 2024-04-22 14:05:35 +00:00
drake 5e1b1e3a1f Update fbla_ui/README.md 2024-04-17 15:37:23 +00:00
drake 501a8113b2 Update readme formatting and packages 2024-04-16 13:56:13 +00:00
drake 9b30a0c16d Update readme formatting 2024-04-10 14:48:33 +00:00
drake 5a560fbae8 Update Readme for WI SLC 2024-04-10 14:43:46 +00:00
23 changed files with 1388 additions and 672 deletions
Vendored
+137
View File
@@ -0,0 +1,137 @@
pipeline {
agent any
stages {
stage('Flutter Cleanup/Update') {
steps {
sh '''cd fbla_ui
flutter upgrade --force
flutter pub upgrade
flutter --version
flutter doctor
flutter clean'''
}
}
stage('Build API & UI') {
parallel {
stage('Build API') {
steps {
sh '''cd fbla-api
docker image prune -f
docker build --no-cache -t fbla-api .'''
}
}
stage('UI Web Build') {
steps {
sh '''cd fbla_ui
flutter build web --release --base-href /fbla/'''
}
}
stage('UI Linux Build') {
steps {
sh '''cd fbla_ui
flutter build linux --release'''
}
}
stage('UI APK Build') {
steps {
sh '''cd fbla_ui
flutter build apk --release'''
}
}
}
}
stage('Deploy and Save') {
parallel {
stage('Save Other Builds') {
steps {
script {
def remote = [
name : 'HostServer',
host : '192.168.0.216',
user : "${env.JOBLINK_LOCAL_USER}",
password : "${env.JOBLINK_LOCAL_PASSWD}",
allowAnyHosts: true,
]
if (env.BRANCH_NAME == 'main') {
sshRemove(path: "/home/${env.JOBLINK_LOCAL_USER}/builds/main/linux", remote: remote)
sshCommand remote: remote, command: "mkdir /home/${env.JOBLINK_LOCAL_USER}/builds/main/linux"
sshPut(from: 'fbla_ui/build/linux/x64/release', into: "/home/${env.JOBLINK_LOCAL_USER}/builds/main/linux", remote: remote)
sshCommand remote: remote, command: "mv /home/${env.JOBLINK_LOCAL_USER}/builds/main/linux/release/* /home/${env.JOBLINK_LOCAL_USER}/builds/main/linux"
sshCommand remote: remote, command: "rm -R /home/${env.JOBLINK_LOCAL_USER}/builds/main/linux/release/"
sshRemove(path: "/home/${env.JOBLINK_LOCAL_USER}/builds/main/apk", remote: remote)
sshCommand remote: remote, command: "mkdir /home/${env.JOBLINK_LOCAL_USER}/builds/main/apk"
sshPut(from: 'fbla_ui/build/app/outputs/apk/release', into: "/home/${env.JOBLINK_LOCAL_USER}/builds/main/apk", remote: remote)
sshCommand remote: remote, command: "mv /home/${env.JOBLINK_LOCAL_USER}/builds/main/apk/release/* /home/${env.JOBLINK_LOCAL_USER}/builds/main/apk"
sshCommand remote: remote, command: "rm -R /home/${env.JOBLINK_LOCAL_USER}/builds/main/apk/release/"
} else {
sshRemove(path: "/home/${env.JOBLINK_LOCAL_USER}/builds/dev/linux", remote: remote)
sshCommand remote: remote, command: "mkdir /home/${env.JOBLINK_LOCAL_USER}/builds/dev/linux"
sshPut(from: 'fbla_ui/build/linux/x64/release', into: "/home/${env.JOBLINK_LOCAL_USER}/builds/dev/linux", remote: remote)
sshCommand remote: remote, command: "mv /home/${env.JOBLINK_LOCAL_USER}/builds/dev/linux/release/* /home/${env.JOBLINK_LOCAL_USER}/builds/dev/linux"
sshCommand remote: remote, command: "rm -R /home/${env.JOBLINK_LOCAL_USER}/builds/dev/linux/release/"
sshRemove(path: "/home/${env.JOBLINK_LOCAL_USER}/builds/dev/apk", remote: remote)
sshCommand remote: remote, command: "mkdir /home/${env.JOBLINK_LOCAL_USER}/builds/dev/apk"
sshPut(from: 'fbla_ui/build/app/outputs/apk/release', into: "/home/${env.JOBLINK_LOCAL_USER}/builds/dev/apk", remote: remote)
sshCommand remote: remote, command: "mv /home/${env.JOBLINK_LOCAL_USER}/builds/dev/apk/release/* /home/${env.JOBLINK_LOCAL_USER}/builds/dev/apk"
sshCommand remote: remote, command: "rm -R /home/${env.JOBLINK_LOCAL_USER}/builds/dev/apk/release/"
}
}
}
}
stage('Deploy Remote Web UI') {
when {
expression {
env.BRANCH_NAME == 'main'
}
}
steps {
script {
def remote = [
name : 'MarinoDev',
host : 'marinodev.com',
port : 21098,
user : "${env.JOBLINK_REMOTE_USER}",
identityFile : '/var/jenkins_home/marinoDevPrivateKey',
passphrase : "${env.JOBLINK_REMOTE_PASSWD}",
allowAnyHosts: true,
]
sshRemove(path: "/home/${env.JOBLINK_REMOTE_USER}/public_html/fbla", remote: remote)
sshPut(from: 'fbla_ui/build/web/', into: "/home/${env.JOBLINK_REMOTE_USER}/public_html/", remote: remote)
sshCommand remote: remote, command: "mv /home/${env.JOBLINK_REMOTE_USER}/public_html/web /home/${env.JOBLINK_REMOTE_USER}/public_html/fbla"
}
}
}
stage('Deploy Local Web UI') {
steps {
script {
def remote = [
name : 'HostServer',
host : '192.168.0.216',
user : "${env.JOBLINK_LOCAL_USER}",
password : "${env.JOBLINK_LOCAL_PASSWD}",
allowAnyHosts: true,
]
sshRemove(path: "/home/${env.JOBLINK_LOCAL_USER}/fbla-webserver/webfiles/fbla", remote: remote)
sshPut(from: 'fbla_ui/build/web/', into: "/home/${env.JOBLINK_LOCAL_USER}/fbla-webserver", remote: remote)
sshCommand remote: remote, command: "mv /home/${env.JOBLINK_LOCAL_USER}/fbla-webserver/web /home/${env.JOBLINK_LOCAL_USER}/fbla-webserver/webfiles/fbla"
}
}
}
stage('Start API') {
steps {
sh '''cd fbla-api
docker-compose down
docker-compose up -d'''
}
}
}
}
stage('Run API Tests') {
steps {
sh '''cd fbla-api
dart pub get
dart run ./test/fbla_api_test.dart'''
}
}
}
}
+54 -3
View File
@@ -1,5 +1,56 @@
This is my app `Job Link` for the 2023-2024 FBLA Coding and Programming event.
It uses the [Flutter Framework](https://flutter.dev/) for the front end and [Dart Language](https://dart.dev/) for both the front end and API.
# Job Link
Please view the README in [fbla_ui](fbla_ui/README.md) and [fbla-api](fbla-api/README.md) for specific instructions for installation and usage for each part of the app
This is my app `Job Link` for the 2023-2024 FBLA Coding and Programming event.\
WI SLC Winner.
It uses the [Flutter Framework](https://flutter.dev/) for the front end and the [Dart Language](https://dart.dev/) for both the front end and API.
## 3rd Party Libraries
- [dart_jsonwebtoken](https://pub.dev/packages/dart_jsonwebtoken)
- [sliver_tools](https://pub.dev/packages/sliver_tools)
- [flutter_sticky_header](https://pub.dev/packages/flutter_sticky_header)
- [pdf](https://pub.dev/packages/pdf)
- [printing](https://pub.dev/packages/printing)
- [open_filex](https://pub.dev/packages/open_filex)
- [postgres](https://pub.dev/packages/postgres)
- [argon2](https://pub.dev/packages/argon2)
## Requirements
### Job Link
- **OS**: Windows, Linux, MacOS, Android, IOS. Note that some releases may not contain a MacOS or IOS build.
- Stable internet connection.
### API
- **OS**: Windows, Linux, MacOS.
- Stable internet connection.
## Installation/Usage
Please view the README in [fbla_ui](fbla_ui/README.md) and [fbla-api](fbla-api/README.md) for specific instructions for installation and usage for each part of the app.
## Competitions
[Here](https://docs.google.com/presentation/d/1ZbSE9RqobU2T-NDIm3CUtT_9nEhOm3_B47NZl1-c_QA) is the presentation used for competitions.
### WI State Leadership Conference
1st Place\
Used release 0.1.1\
[Here](SLC) are my score sheets.
Questions asked (with my answers):
**Q**: Why did you decide to use the apps (framework and tools I assume) you used?\
**A**: I decided to use Flutter primarily because of its cross-platform capabilities and its integration with the [Material design specification](https://m3.material.io/). I also decided to use it because I had some previous experience with Flutter and Dart, and Dart is somewhat similar to Java which I also had experience with.
This one seems to just be a clarification for the `User input is validated` category of the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (page 5-6)\
**Q**: You mentioned that you validated that the description [of the business] is required, do you validate all the inputs or just that?\
**A**: I validate that all of the required fields are inputted, and I also check for a valid email address and website format.
This seems to be for the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (page 5-6) `Code Quality` section.\
While looking through a binder of the presentation and source code:\
**Q**: Is your source code all properly commented?\
**A**: While it is hard to comment UI code because of the nature of it, I did my best to use comments where applicable, and I also provide extensive documentation of the installation and usage of the app in the documentation in the README files.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -9,7 +9,7 @@ RUN echo 'deb [signed-by=/usr/share/keyrings/dart.gpg arch=amd64] https://storag
RUN apt-get update && apt-get install -y dart
ENV PATH="$PATH:/usr/lib/dart/bin"
RUN cd /root && git clone https://https://git.marinodev.com/MarinoDev/FBLA24.git
RUN cd /root && git clone https://git.marinodev.com/MarinoDev/FBLA24.git
WORKDIR /root/FBLA24/fbla-api
RUN dart pub install
-1
View File
@@ -16,6 +16,5 @@ docker-compose up -d'''
dart run ./test/fbla_api_test.dart'''
}
}
}
}
+30 -20
View File
@@ -2,19 +2,16 @@ This is the API for my 2023-2024 FBLA Coding & Programming App
## Installation
1. Install [Dart SDK](https://dart.dev/get-dart)
2. Install [PostgreSQL](https://www.postgresql.org/) and set it up
3. Clone the repo
```
1. Install [Dart SDK](https://dart.dev/get-dart).
2. Clone the repo .
```bash
git clone https://git.marinodev.com/MarinoDev/FBLA24.git
cd FBLA24/fbla-api/
```
4. Run `dart pub install` to install dart packages
## Usage
1. Create `fbla` database in postgres
```
3. Run `dart pub install` to install dart packages.
4. Install [PostgreSQL](https://www.postgresql.org/) and set it up.
5. Create `fbla` database in postgres.
```SQL
CREATE DATABASE fbla
WITH
OWNER = [username]
@@ -24,8 +21,8 @@ CREATE DATABASE fbla
```
Make sure to change [username] to the actual username of your postgres instance.
2. Create `businesses` table
```
6. Create `businesses` table.
```SQL
-- Table: public.businesses
-- DROP TABLE IF EXISTS public.businesses;
@@ -51,10 +48,10 @@ TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.businesses
OWNER to [username];
```
Make sure to change [username] to the actual username of your postgres instance
Make sure to change [username] to the actual username of your postgres instance.
3. Create `users` table
```
7. Create `users` table.
```SQL
-- Table: public.users
-- DROP TABLE IF EXISTS public.users;
@@ -73,9 +70,22 @@ TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.users
OWNER to postgres;
```
4. Set environment variables `POSTGRES_ADDRESS` (IP address), `POSTGRES_PORT` (default 5432), `POSTGRES_USERNAME`, and `POSTGRES_PASSWORD` to appropriate information for your postgres database. Also set `SECRET_KEY` to anything you want to genera te JSON Web Tokens.
5. Set lib/create_first_user.dart username and password variables at top of file and run with `dart run lib/create_first_user.dart`
Note: the username and password you use here will be what you use to log into the app as an admin
6. Your database should be all set up! Start the API with `dart run lib/fbla_api.dart` or alternatively, run it in a docker container with [Docker Build](https://docs.docker.com/reference/cli/docker/image/build/) and [Docker Compose](https://docs.docker.com/compose/) using the included [Dockerfile](Dockerfile) and [docker-compose.yml](docker-compose.yml) files. If using `Docker Compose`, change the first portion of the volumes path on line 9 to any desired storage location.
8. Set environment variables `JOBLINK_POSTGRES_ADDRESS` (IP address), `JOBLINK_POSTGRES_PORT` (default 5432), `JOBLINK_POSTGRES_USERNAME`, and `JOBLINK_POSTGRES_PASSWORD` to appropriate information for your postgres database. Also set `JOBLINK_SECRET_KEY` to anything you want to generate JSON Web Tokens.
9. Set lib/create_first_user.dart username and password variables at top of file and run with `dart run lib/create_first_user.dart`.\
Note: the username and password you use here will be what you use to log into the app as an admin.
## Usage
### Installed
Start the API with `dart run lib/fbla_api.dart`\
Note the address it is serving at, you will need this to connect it to the UI, and to run tests.
7. Test your api with `dart run test/fbla_api_test.dart`, setting `apiAddress` on line 8 to your api address.
### Docker
Run it in a docker container with [Docker Build](https://docs.docker.com/reference/cli/docker/image/build/) and [Docker Compose](https://docs.docker.com/compose/) using the included [Dockerfile](Dockerfile) and [docker-compose.yml](docker-compose.yml) files. If using `Docker Compose`, change the first portion of the volumes path on line 9 to any desired storage location.\
Note the address it is serving at, you will need this to connect it to the UI, and to run tests.
### Testing
You can test all functionality of your api with `dart run test/fbla_api_test.dart`, setting `apiAddress` on line 8 to your api address.
+6 -5
View File
@@ -3,13 +3,14 @@ version: '3'
services:
fbla_api:
image: fbla-api
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- /var/jenkins_home/logos:/root/FBLA24/fbla-api/logos
environment:
- POSTGRES_USERNAME
- POSTGRES_PASSWORD
- SECRET_KEY
- POSTGRES_ADDRESS
- POSTGRES_PORT
- JOBLINK_POSTGRES_USERNAME
- JOBLINK_POSTGRES_PASSWORD
- JOBLINK_SECRET_KEY
- JOBLINK_POSTGRES_ADDRESS
- JOBLINK_POSTGRES_PORT
+4 -4
View File
@@ -20,11 +20,11 @@ var parameters = Argon2Parameters(
);
final postgres = PostgreSQLConnection(
Platform.environment['POSTGRES_ADDRESS']!,
int.parse(Platform.environment['POSTGRES_PORT']!),
Platform.environment['JOBLINK_POSTGRES_ADDRESS']!,
int.parse(Platform.environment['JOBLINK_POSTGRES_PORT']!),
'fbla',
username: Platform.environment['POSTGRES_USERNAME'],
password: Platform.environment['POSTGRES_PASSWORD'],
username: Platform.environment['JOBLINK_POSTGRES_USERNAME'],
password: Platform.environment['JOBLINK_POSTGRES_PASSWORD'],
);
Future<void> main() async {
+266 -93
View File
@@ -2,16 +2,16 @@ import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:argon2/argon2.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:http/http.dart' as http;
import 'package:postgres/postgres.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
import 'package:http/http.dart' as http;
import 'package:argon2/argon2.dart';
SecretKey secretKey = SecretKey(Platform.environment['SECRET_KEY']!);
SecretKey secretKey = SecretKey(Platform.environment['JOBLINK_SECRET_KEY']!);
enum BusinessType {
food,
@@ -22,47 +22,37 @@ enum BusinessType {
other,
}
enum JobType { cashier, server, mechanic, other }
class Business {
int id;
String name;
String description;
BusinessType type;
String website;
String contactName;
String contactEmail;
String contactPhone;
String notes;
String locationName;
String locationAddress;
String? website;
String? contactName;
String? contactEmail;
String? contactPhone;
String? notes;
String? locationName;
String? locationAddress;
Business({
required this.id,
Business(
{required this.id,
required this.name,
required this.description,
required this.type,
required this.website,
required this.contactName,
required this.contactEmail,
required this.contactPhone,
required this.notes,
required this.locationName,
required this.locationAddress,
});
this.website,
this.contactName,
this.contactEmail,
this.contactPhone,
this.notes,
this.locationName,
this.locationAddress});
factory Business.fromJson(Map<String, dynamic> json) {
bool typeValid = true;
try {
BusinessType.values.byName(json['type']);
} catch (e) {
typeValid = false;
}
return Business(
id: json['id'],
name: json['name'],
description: json['description'],
type: typeValid
? BusinessType.values.byName(json['type'])
: BusinessType.other,
website: json['website'],
contactName: json['contactName'],
contactEmail: json['contactEmail'],
@@ -74,9 +64,46 @@ class Business {
}
}
class JobListing {
String? id;
String? businessId;
String name;
String description;
JobType type;
String? wage;
String? link;
JobListing(
{this.id,
this.businessId,
required this.name,
required this.description,
required this.type,
this.wage,
this.link});
factory JobListing.fromJson(Map<String, dynamic> json) {
bool typeValid = true;
try {
JobType.values.byName(json['type']);
} catch (e) {
typeValid = false;
}
return JobListing(
id: json['id'],
businessId: json['businessId'],
name: json['name'],
description: json['description'],
type: typeValid ? JobType.values.byName(json['type']) : JobType.other,
wage: json['wage'],
link: json['link'],
);
}
}
Future<String> fetchBusinessData() async {
final result = await postgres.query(
'''
final result = await postgres.query('''
SELECT json_agg(
json_build_object(
'id', id,
@@ -92,12 +119,9 @@ Future<String> fetchBusinessData() async {
'locationAddress', "locationAddress"
)
) FROM businesses
'''
);
''');
var encoded = json.encode(result);
var decoded = json.decode(encoded);
encoded = json.encode(decoded[0][0]);
var encoded = json.encode(result[0][0]);
return encoded;
}
@@ -105,17 +129,14 @@ Future<String> fetchBusinessData() async {
String _hostname = 'localhost';
const _port = 8000;
final postgres = PostgreSQLConnection(
Platform.environment['POSTGRES_ADDRESS']!,
int.parse(Platform.environment['POSTGRES_PORT']!),
Platform.environment['JOBLINK_POSTGRES_ADDRESS']!,
int.parse(Platform.environment['JOBLINK_POSTGRES_PORT']!),
'fbla',
username: Platform.environment['POSTGRES_USERNAME'],
password: Platform.environment['POSTGRES_PASSWORD'],
username: Platform.environment['JOBLINK_POSTGRES_USERNAME'],
password: Platform.environment['JOBLINK_POSTGRES_PASSWORD'],
);
void main() async {
await postgres.open();
@@ -130,12 +151,95 @@ void main() async {
headers: {'Access-Control-Allow-Origin': '*'},
);
});
app.get('/fbla-api/businessdata/overview', (Request request) async {
print('business overview request received');
var filters = request.url.queryParameters['filters']?.split(',') ??
JobType.values.asNameMap().keys;
// List<Map<String, List<Map<String, dynamic>>>> this is the real type lol
List<dynamic> output = [];
for (int i = 0; i < filters.length; i++) {
var postgresResult = (await postgres.query('''
SELECT json_agg(
json_build_object(
'id', id,
'name', name,
'description', description,
'website', website,
'contactEmail', "contactEmail",
'contactPhone', "contactPhone",
'locationName', "locationName"
)
) FROM public.businesses WHERE id IN (SELECT "businessId" FROM public.listings WHERE type='${filters.elementAt(i)}')
'''))[0][0];
if (postgresResult != null) {
output.add({filters.elementAt(i): postgresResult});
}
}
return Response.ok(
json.encode(output),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain'
},
);
});
app.get('/fbla-api/businessdata/business/<business>',
(Request request, String business) async {
print('idividual business data request received');
var result = (await postgres.query('''
SELECT
json_build_object(
'id', b.id,
'name', b.name,
'description', b.description,
'website', b.website,
'contactName', b."contactName",
'contactEmail', b."contactEmail",
'contactPhone', b."contactPhone",
'notes', b.notes,
'locationName', b."locationName",
'locationAddress', b."locationAddress",
'listings',
json_agg(
json_build_object(
'id', l.id,
'name', l.name,
'description', l.description,
'type', l.type,
'wage', l.wage,
'link', l.link
)
)
)
FROM businesses b
LEFT JOIN listings l ON b.id = l.business_id
WHERE b.id = $business
GROUP BY b.id;
'''))[0][0];
return Response.ok(
json.encode(result),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain'
},
);
});
app.get('/fbla-api/businessdata', (Request request) async {
print('business data request received');
final output = await fetchBusinessData();
return Response.ok(
output.toString(),
headers: {'Access-Control-Allow-Origin': '*'},
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain'
},
);
});
app.get('/fbla-api/logos/<logo>', (Request request, String logoId) {
@@ -162,15 +266,12 @@ void main() async {
var json = jsonDecode(payload);
Business business = Business.fromJson(json);
await postgres.query(
'''
INSERT INTO businesses (name, description, type, website, "contactName", "contactPhone", "contactEmail", notes, "locationName", "locationAddress")
VALUES ('${business.name.replaceAll("'", "''")}', '${business.description.replaceAll("'", "''")}', '${business.type.name}', '${business.website}', '${business.contactName.replaceAll("'", "''")}', '${business.contactPhone}', '${business.contactEmail}', '${business.notes.replaceAll("'", "''")}', '${business.locationName.replaceAll("'", "''")}', '${business.locationAddress.replaceAll("'", "''")}')
'''
);
await postgres.query('''
INSERT INTO businesses (name, description, website, "contactName", "contactPhone", "contactEmail", notes, "locationName", "locationAddress")
VALUES ('${business.name.replaceAll("'", "''")}', '${business.description.replaceAll("'", "''")}', '${business.website ?? 'NULL'}', '${business.contactName?.replaceAll("'", "''") ?? 'NULL'}', '${business.contactPhone ?? 'NULL'}', '${business.contactEmail ?? 'NULL'}', '${business.notes?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationName?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationAddress?.replaceAll("'", "''") ?? 'NULL'}')
''');
final dbBusiness = await postgres.query(
'''SELECT * FROM public.businesses
final dbBusiness = await postgres.query('''SELECT * FROM public.businesses
ORDER BY id DESC LIMIT 1''');
var id = dbBusiness[0][0];
var logoResponse = await http.get(
@@ -195,10 +296,43 @@ void main() async {
headers: {'Access-Control-Allow-Origin': '*'},
);
});
app.post('/fbla-api/createlisting', (Request request) async {
print('create business request received');
final payload = await request.readAsString();
var auth = request.headers['Authorization']?.replaceAll('Bearer ', '');
try {
JWT.verify(auth!, secretKey);
var json = jsonDecode(payload);
JobListing listing = JobListing.fromJson(json);
await postgres.query('''
INSERT INTO listings ("businessId", name, description, type, wage, link)
VALUES ('${listing.businessId}' '${listing.name.replaceAll("'", "''")}', '${listing.description.replaceAll("'", "''")}', '${listing.type.name}', '${listing.wage ?? 'NULL'}', '${listing.link?.replaceAll("'", "''") ?? 'NULL'}')
''');
final dbListing = await postgres.query('''SELECT id FROM public.listings
ORDER BY id DESC LIMIT 1''');
var id = dbListing[0][0];
return Response.ok(
id.toString(),
headers: {'Access-Control-Allow-Origin': '*'},
);
} on JWTExpiredException {
print('JWT Expired');
} on JWTException catch (e) {
print(e.message);
}
return Response.unauthorized(
'unauthorized',
headers: {'Access-Control-Allow-Origin': '*'},
);
});
app.post('/fbla-api/deletebusiness', (Request request) async {
print('delete business request received');
final payload = await request.readAsString();
var auth = request.headers['Authorization']?.replaceAll('Bearer ', '');
try {
@@ -206,13 +340,7 @@ void main() async {
var json = jsonDecode(payload);
var id = json['id'];
await postgres.query(
'''
DELETE FROM public.businesses
WHERE id IN
($id);
'''
);
await postgres.query('DELETE FROM public.businesses WHERE id=$id;');
try {
await File('logos/$id.png').delete();
@@ -235,6 +363,33 @@ void main() async {
headers: {'Access-Control-Allow-Origin': '*'},
);
});
app.post('/fbla-api/deletelisting', (Request request) async {
print('delete listing request received');
final payload = await request.readAsString();
var auth = request.headers['Authorization']?.replaceAll('Bearer ', '');
try {
JWT.verify(auth!, secretKey);
var json = jsonDecode(payload);
var id = json['id'];
await postgres.query('DELETE FROM public.listings WHERE id=$id;');
return Response.ok(
id.toString(),
headers: {'Access-Control-Allow-Origin': '*'},
);
} on JWTExpiredException {
print('JWT Expired');
} on JWTException catch (e) {
print(e.message);
}
return Response.unauthorized(
'unauthorized',
headers: {'Access-Control-Allow-Origin': '*'},
);
});
app.post('/fbla-api/editbusiness', (Request request) async {
print('edit business request received');
@@ -246,13 +401,11 @@ void main() async {
var json = jsonDecode(payload);
Business business = Business.fromJson(json);
await postgres.query(
'''
await postgres.query('''
UPDATE businesses SET
name = '${business.name.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, description = '${business.description.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, website = '${business.website}'::text, type = '${business.type.name}'::text, "contactName" = '${business.contactName.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "contactPhone" = '${business.contactPhone}'::text, "contactEmail" = '${business.contactEmail}'::text, notes = '${business.notes.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationName" = '${business.locationName.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationAddress" = '${business.locationAddress.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text WHERE
name = '${business.name.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, description = '${business.description.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, website = '${business.website!}'::text, "contactName" = '${business.contactName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "contactPhone" = '${business.contactPhone!}'::text, "contactEmail" = '${business.contactEmail!}'::text, notes = '${business.notes!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationName" = '${business.locationName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationAddress" = '${business.locationAddress!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text WHERE
id = ${business.id};
'''
);
''');
var logoResponse = await http.get(
Uri.http('logo.clearbit.com', '/${business.website}'),
@@ -264,11 +417,10 @@ void main() async {
print('Failure to delete logo! $e');
}
if (logoResponse.headers.toString().contains('image/png')) {
await File('logos/${business.id}.png').writeAsBytes(logoResponse.bodyBytes);
await File('logos/${business.id}.png')
.writeAsBytes(logoResponse.bodyBytes);
}
return Response.ok(
business.id.toString(),
headers: {'Access-Control-Allow-Origin': '*'},
@@ -284,6 +436,38 @@ void main() async {
headers: {'Access-Control-Allow-Origin': '*'},
);
});
app.post('/fbla-api/editlisting', (Request request) async {
print('edit listing request received');
final payload = await request.readAsString();
var auth = request.headers['Authorization']?.replaceAll('Bearer ', '');
try {
JWT.verify(auth!, secretKey);
var json = jsonDecode(payload);
JobListing listing = JobListing.fromJson(json);
await postgres.query('''
UPDATE listings SET
"businessId" = ${listing.businessId}, name = '${listing.name.replaceAll("'", "''")}'::text, description = '${listing.description.replaceAll("'", "''")}'::text, type = '${listing.type.name}'::text, wage = '${listing.wage ?? 'NULL'}'::text, link = '${listing.link?.replaceAll("'", "''") ?? 'NULL'}'::text WHERE
id = ${listing.id};
''');
return Response.ok(
listing.id.toString(),
headers: {'Access-Control-Allow-Origin': '*'},
);
} on JWTExpiredException {
print('JWT Expired');
} on JWTException catch (e) {
print(e.message);
}
return Response.unauthorized(
'unauthorized',
headers: {'Access-Control-Allow-Origin': '*'},
);
});
app.post('/fbla-api/signin', (Request request) async {
print('signin request received');
@@ -292,9 +476,8 @@ void main() async {
var username = json['username'];
var password = json['password'];
var saltDb = await postgres.query(
'SELECT salt FROM users WHERE username=\'$username\''
);
var saltDb = await postgres
.query('SELECT salt FROM users WHERE username=\'$username\'');
if (saltDb.isEmpty) {
return Response.unauthorized(
'invalid username',
@@ -320,17 +503,13 @@ void main() async {
argon2.generateBytes(passwordBytes, result);
var resultHex = result.toHexString();
var passwordHashDb = await postgres.query(
'SELECT password_hash FROM users WHERE username=\'$username\''
);
var passwordHashDb = await postgres
.query('SELECT password_hash FROM users WHERE username=\'$username\'');
var passwordHash = passwordHashDb[0][0].toString();
if (passwordHash == resultHex) {
final jwt = JWT(
{
'username': username
},
{'username': username},
);
final token = jwt.sign(secretKey);
try {
@@ -345,7 +524,6 @@ void main() async {
token.toString(),
headers: {'Access-Control-Allow-Origin': '*'},
);
} else {
return Response.unauthorized(
'invalid password',
@@ -367,7 +545,8 @@ void main() async {
var password = json['password'];
var r = Random.secure();
String randomSalt = String.fromCharCodes(List.generate(32, (index) => r.nextInt(33) + 89));
String randomSalt = String.fromCharCodes(
List.generate(32, (index) => r.nextInt(33) + 89));
final salt = randomSalt.toBytesLatin1();
var parameters = Argon2Parameters(
@@ -385,18 +564,15 @@ void main() async {
argon2.generateBytes(passwordBytes, result);
var resultHex = result.toHexString();
postgres.query(
'''
postgres.query('''
INSERT INTO public.users (username, password_hash, salt)
VALUES ('$username', '$resultHex', '$randomSalt')
'''
);
''');
return Response.ok(
username,
headers: {'Access-Control-Allow-Origin': '*'},
);
} on JWTExpiredException {
print('JWT Expired');
} on JWTException catch (e) {
@@ -420,18 +596,15 @@ void main() async {
var json = jsonDecode(payload);
var username = json['username'];
postgres.query(
'''
postgres.query('''
DELETE FROM public.users
WHERE username IN ('$username');
'''
);
''');
return Response.ok(
username,
headers: {'Access-Control-Allow-Origin': '*'},
);
} on JWTExpiredException {
print('JWT Expired');
} on JWTException catch (e) {
+82 -45
View File
@@ -5,10 +5,15 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3"
url: "https://pub.dev"
source: hosted
version: "64.0.0"
version: "68.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.1.0"
adaptive_number:
dependency: transitive
description:
@@ -21,10 +26,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808"
url: "https://pub.dev"
source: hosted
version: "6.2.0"
version: "6.5.0"
argon2:
dependency: "direct main"
description:
@@ -37,10 +42,10 @@ packages:
dependency: transitive
description:
name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.5.0"
async:
dependency: transitive
description:
@@ -61,10 +66,10 @@ packages:
dependency: transitive
description:
name: buffer
sha256: "8962c12174f53e2e848a6acd7ac7fd63d8a1a6a316c20c458a832d87eba5422a"
sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.3"
charcode:
dependency: transitive
description:
@@ -73,6 +78,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
@@ -93,10 +106,10 @@ packages:
dependency: transitive
description:
name: coverage
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e"
url: "https://pub.dev"
source: hosted
version: "1.6.3"
version: "1.8.0"
crypto:
dependency: transitive
description:
@@ -109,10 +122,10 @@ packages:
dependency: "direct main"
description:
name: dart_jsonwebtoken
sha256: "6703695f581fc54d0a7e5f281c5538735167605bb9e5abd208c8b330625a92b1"
sha256: "346e9a21e4bf6e6a431e19ece00ebb2e3668e1e339cabdf6f46d18d88692a848"
url: "https://pub.dev"
source: hosted
version: "2.12.1"
version: "2.14.0"
ed25519_edwards:
dependency: transitive
description:
@@ -141,10 +154,10 @@ packages:
dependency: transitive
description:
name: frontend_server_client
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "4.0.0"
glob:
dependency: transitive
description:
@@ -165,10 +178,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.2.1"
http_methods:
dependency: transitive
description:
@@ -205,10 +218,10 @@ packages:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.6.7"
version: "0.7.1"
lints:
dependency: "direct dev"
description:
@@ -225,30 +238,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
macros:
dependency: transitive
description:
name: macros
sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79"
url: "https://pub.dev"
source: hosted
version: "0.1.0-main.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.16+1"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.15.0"
mime:
dependency: transitive
description:
name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.0.5"
node_preamble:
dependency: transitive
description:
@@ -269,10 +290,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
pedantic:
dependency: transitive
description:
@@ -285,10 +306,10 @@ packages:
dependency: transitive
description:
name: pointycastle
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.7.3"
version: "3.9.1"
pool:
dependency: transitive
description:
@@ -301,10 +322,10 @@ packages:
dependency: "direct main"
description:
name: postgres
sha256: "98457afc06dd3f9d6892c178ea03ca9659e454107c9be90111e607691998d70d"
sha256: f8e4f14734d096277f77ed5dddefcbc1ce18f8f7db5b7ff4b5dd6df2d9db2730
url: "https://pub.dev"
source: hosted
version: "2.6.2"
version: "2.6.4"
pub_semver:
dependency: transitive
description:
@@ -365,10 +386,10 @@ packages:
dependency: transitive
description:
name: shelf_web_socket
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "2.0.0"
source_map_stack_trace:
dependency: transitive
description:
@@ -429,26 +450,26 @@ packages:
dependency: "direct dev"
description:
name: test
sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9"
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
url: "https://pub.dev"
source: hosted
version: "1.24.6"
version: "1.25.7"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.2"
test_core:
dependency: transitive
description:
name: test_core
sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265"
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
url: "https://pub.dev"
source: hosted
version: "0.5.6"
version: "0.6.4"
typed_data:
dependency: transitive
description:
@@ -469,10 +490,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d"
sha256: "360c4271613beb44db559547d02f8b0dc044741d0eeb9aa6ccdb47e8ec54c63a"
url: "https://pub.dev"
source: hosted
version: "11.9.0"
version: "14.2.3"
watcher:
dependency: transitive
description:
@@ -481,22 +502,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "3.0.0"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
yaml:
dependency: transitive
description:
@@ -506,4 +543,4 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.0.6 <4.0.0"
dart: ">=3.4.0-256.0.dev <4.0.0"
+12 -15
View File
@@ -1,18 +1,17 @@
import 'dart:convert';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:fbla_api/fbla_api.dart';
import 'package:test/test.dart';
import 'package:http/http.dart' as http;
import 'dart:io';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:fbla_api/fbla_api.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
final apiAddress = 'https://homelab.marinodev.com';
SecretKey secretKey = SecretKey(Platform.environment['SECRET_KEY']!);
SecretKey secretKey = SecretKey(Platform.environment['JOBLINK_SECRET_KEY']!);
void main() async {
final jwt = JWT(
{
'username': 'tmp'
},
{'username': 'tmp'},
);
final token = jwt.sign(secretKey);
@@ -43,7 +42,7 @@ void main() async{
"password": "tmp"
}
''';
var response = await http.post(Uri.parse('$apiAddress, /fbla-api/createuser'),
var response = await http.post(Uri.parse('$apiAddress/fbla-api/createuser'),
body: json,
headers: {'Authorization': token}).timeout(const Duration(seconds: 20));
expect(response.statusCode, 200);
@@ -70,11 +69,9 @@ void main() async{
"username": "tmp"
}
''';
var response = await http.post(
Uri.parse('$apiAddress/fbla-api/deleteuser'),
var response = await http.post(Uri.parse('$apiAddress/fbla-api/deleteuser'),
body: json,
headers: {'Authorization': token}).timeout(const Duration(seconds: 20)
);
headers: {'Authorization': token}).timeout(const Duration(seconds: 20));
expect(response.statusCode, 200);
expect(response.body, 'tmp');
});
@@ -84,7 +81,6 @@ void main() async{
"id": 0,
"name": "tmp",
"description": "tmp",
"type": "business",
"website": "tmp",
"contactName": "tmp",
"contactEmail": "tmp",
@@ -94,7 +90,8 @@ void main() async{
"locationAddress": "tmp"
}
''';
var response = await http.post(Uri.parse('$apiAddress/fbla-api/createbusiness'),
var response = await http.post(
Uri.parse('$apiAddress/fbla-api/createbusiness'),
body: json,
headers: {'Authorization': token}).timeout(const Duration(seconds: 20));
+17
View File
@@ -6,6 +6,8 @@ My version of the app can be found at [marinodev.com/fbla/](https://marinodev.co
Pre-built files for several operating systems (built with my API) can be found in the releases tab,
alternatively, you can compile it yourself for use with your own API and database (see API readme):
### Windows/Linux
1. Install [Flutter](https://docs.flutter.dev/get-started/install)
2. Use `flutter doctor` to make sure your environment is fully set up for your platform
3. Clone the repo
@@ -18,3 +20,18 @@ cd FBLA24/fbla_ui/
4. Run `flutter pub get` to install all dependencies
5. Optional: set `apiAddress` at the top of `lib/api_logic.dart`
6. Build app with `flutter build --release`
### MacOS
*These instructions are temporary and will change as I try MacOS*
1. Install [Flutter](https://docs.flutter.dev/get-started/install)
2. Use `flutter doctor` to make sure your environment is fully set up for your platform
3. Clone the repo
```
git clone https://git.marinodev.com/MarinoDev/FBLA24.git
cd FBLA24/fbla_ui/
```
4. Run `flutter pub get` to install all dependencies
5. Optional: set `apiAddress` at the top of `lib/api_logic.dart`
6. Build app with `flutter build --release`
+49 -6
View File
@@ -8,8 +8,6 @@ import 'package:http/http.dart' as http;
var apiAddress = 'https://homelab.marinodev.com/fbla-api';
var client = http.Client();
// var apiAddress = '192.168.0.114:8000';
// var apiLogoAddress = 'https://homelab.marinodev.com/fbla-api';
// var apiLogoAddress = 'http://192.168.0.114:8000';
Future fetchBusinessData() async {
try {
@@ -31,13 +29,59 @@ Future fetchBusinessData() async {
}
}
Future fetchBusinessDataOverview() async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata/overview'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
Map<JobType, List<Business>> groupedBusinesses = {};
for (int i = 0; i < decodedResponse.length; i++) {
groupedBusinesses.addAll({
JobType.values.byName(decodedResponse[i]):
decodedResponse.map((json) => Business.fromJson(json)).toList()
});
}
return groupedBusinesses;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future fetchBusiness(int id) async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata/business/$id'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
Business business = Business.fromJson(decodedResponse);
return business;
} else {
return 'Error ${response.statusCode}! Please try again later!';
}
} on TimeoutException {
return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
}
}
Future createBusiness(Business business, String jwt) async {
var json = '''
{
"id": ${business.id},
"name": "${business.name}",
"description": "${business.description}",
"type": "${business.type.name}",
"website": "${business.website}",
"contactName": "${business.contactName}",
"contactEmail": "${business.contactEmail}",
@@ -62,10 +106,10 @@ Future createBusiness(Business business, String jwt) async {
}
}
Future deleteBusiness(Business business, String jwt) async {
Future deleteBusiness(int id, String jwt) async {
var json = '''
{
"id": ${business.id}
"id": $id
}
''';
@@ -89,7 +133,6 @@ Future editBusiness(Business business, String jwt) async {
"id": ${business.id},
"name": "${business.name}",
"description": "${business.description}",
"type": "${business.type.name}",
"website": "${business.website}",
"contactName": "${business.contactName}",
"contactEmail": "${business.contactEmail}",
+16 -14
View File
@@ -22,15 +22,15 @@ class Home extends StatefulWidget {
}
class _HomeState extends State<Home> {
late Future refreshBusinessDataFuture;
late Future refreshBusinessDataOverviewFuture;
bool _isPreviousData = false;
late List<Business> businesses;
late Map<JobType, List<Business>> overviewBusinesses;
@override
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessData();
refreshBusinessDataOverviewFuture = fetchBusinessDataOverview();
initialLogin();
}
@@ -65,10 +65,10 @@ class _HomeState extends State<Home> {
body: RefreshIndicator(
edgeOffset: 120,
onRefresh: () async {
var refreshedData = fetchBusinessData();
var refreshedData = fetchBusinessDataOverview();
await refreshedData;
setState(() {
refreshBusinessDataFuture = refreshedData;
refreshBusinessDataOverviewFuture = refreshedData;
});
},
child: CustomScrollView(
@@ -185,8 +185,8 @@ class _HomeState extends State<Home> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ExportData(businesses: businesses)));
builder: (context) => ExportData(
groupedBusinesses: overviewBusinesses)));
}
},
),
@@ -245,7 +245,7 @@ class _HomeState extends State<Home> {
],
),
FutureBuilder(
future: refreshBusinessDataFuture,
future: refreshBusinessDataOverviewFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
@@ -263,9 +263,11 @@ class _HomeState extends State<Home> {
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
var refreshedData = fetchBusinessData();
var refreshedData =
fetchBusinessDataOverview();
setState(() {
refreshBusinessDataFuture = refreshedData;
refreshBusinessDataOverviewFuture =
refreshedData;
});
},
),
@@ -274,11 +276,11 @@ class _HomeState extends State<Home> {
));
}
businesses = snapshot.data;
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return BusinessDisplayPanel(
businesses: businesses,
groupedBusinesses: overviewBusinesses,
widescreen: widescreen,
selectable: false);
} else if (snapshot.hasError) {
@@ -293,7 +295,7 @@ class _HomeState extends State<Home> {
ConnectionState.waiting) {
if (_isPreviousData) {
return BusinessDisplayPanel(
businesses: businesses,
groupedBusinesses: overviewBusinesses,
widescreen: widescreen,
selectable: false);
} else {
@@ -339,7 +341,7 @@ class _HomeState extends State<Home> {
Widget? _getFAB() {
if (loggedIn) {
return FloatingActionButton(
child: const Icon(Icons.add),
child: const Icon(Icons.add_business),
onPressed: () {
Navigator.push(
context,
+160 -30
View File
@@ -4,28 +4,113 @@ import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/pages/signin_page.dart';
import 'package:fbla_ui/shared.dart';
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:url_launcher/url_launcher.dart';
class BusinessDetail extends StatefulWidget {
final Business inputBusiness;
final int id;
final String name;
final JobType clickFromType;
const BusinessDetail({super.key, required this.inputBusiness});
const BusinessDetail(
{super.key,
required this.id,
required this.name,
required this.clickFromType});
@override
State<BusinessDetail> createState() => _CreateBusinessDetailState();
}
class _CreateBusinessDetailState extends State<BusinessDetail> {
late Future loadBusiness;
@override
void initState() {
super.initState();
loadBusiness = fetchBusiness(widget.id);
}
@override
Widget build(BuildContext context) {
Business business = Business.copy(widget.inputBusiness);
return FutureBuilder(
future: loadBusiness,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType != String) {
return Scaffold(
appBar: AppBar(
title: Text(business.name),
actions: _getActions(business),
title: Text(snapshot.data.name),
actions: _getActions(snapshot.data, widget.clickFromType),
),
body: ListView(
body: _detailBody(snapshot.data),
);
} else {
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child:
Text(snapshot.data, textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
var refreshedData = fetchBusiness(widget.id);
setState(() {
loadBusiness = refreshedData;
});
},
),
),
]),
),
);
}
}
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child:
RiveAnimation.asset('assets/mdev_triangle_loading.riv'),
)),
);
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
});
}
ListView _detailBody(Business business) {
return ListView(
children: [
// Title, logo, desc, website
Card(
clipBehavior: Clip.antiAlias,
child: Column(
@@ -41,12 +126,11 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${business.id}',
child: Image.network('$apiAddress/logos/${business.id}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48,
return getIconFromJobType(widget.clickFromType, 48,
Theme.of(context).colorScheme.onSurface);
}),
),
@@ -54,7 +138,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
ListTile(
leading: const Icon(Icons.link),
title: const Text('Website'),
subtitle: Text(business.website,
subtitle: Text(business.website!,
style: const TextStyle(color: Colors.blue)),
onTap: () {
launchUrl(Uri.parse('https://${business.website}'));
@@ -63,9 +147,53 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
],
),
),
// Available positions
Card(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text(
'Available Postitions',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
// Container(
// height: 400,
// width: 300,
ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: [
ListTile(
title: Text('Postition 1'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
ListTile(
title: Text('Postition 2'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
ListTile(
title: Text('Postition 3'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
],
),
]),
),
),
// Contact info
Visibility(
visible: (business.contactEmail.isNotEmpty ||
business.contactPhone.isNotEmpty),
visible:
(business.contactEmail != null || business.contactPhone != null),
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
@@ -75,9 +203,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text(
business.contactName.isEmpty
? 'Contact ${business.name}'
: business.contactName,
business.contactName ?? 'Contact ${business.name}',
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
@@ -86,10 +212,11 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
],
),
Visibility(
visible: business.contactPhone.isNotEmpty,
visible: business.contactPhone != null,
child: ListTile(
leading: Icon(Icons.phone),
title: Text(business.contactPhone),
title: Text(business.contactPhone!),
// maybe replace ! with ?? ''. same is true for below
onTap: () {
showDialog(
context: context,
@@ -97,10 +224,10 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.background,
title: Text(business.contactName.isEmpty
title: Text(business.contactName!.isEmpty
? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'),
content: Text(business.contactName.isEmpty
content: Text(business.contactName!.isEmpty
? 'Would you like to call or text ${business.name}?'
: 'Would you like to call or text ${business.contactName}?'),
actions: [
@@ -125,10 +252,10 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
),
),
Visibility(
visible: business.contactEmail.isNotEmpty,
visible: business.contactEmail != null,
child: ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail),
title: Text(business.contactEmail!),
onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}'));
},
@@ -138,13 +265,14 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
),
),
),
// Location
Visibility(
child: Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: const Icon(Icons.location_on),
title: Text(business.locationName),
subtitle: Text(business.locationAddress),
title: Text(business.locationName!),
subtitle: Text(business.locationAddress!),
onTap: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
@@ -152,8 +280,9 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
),
),
),
// Notes
Visibility(
visible: business.notes.isNotEmpty,
visible: business.notes != null,
child: Card(
child: ListTile(
leading: const Icon(Icons.notes),
@@ -161,24 +290,25 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
'Additional Notes:',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
subtitle: Text(business.notes),
subtitle: Text(business.notes!),
),
),
),
],
),
);
}
List<Widget>? _getActions(Business business) {
List<Widget>? _getActions(Business business, JobType clickFromType) {
if (loggedIn) {
return [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
CreateEditBusiness(inputBusiness: business)));
builder: (context) => CreateEditBusiness(
inputBusiness: business,
clickFromType: clickFromType,
)));
},
),
IconButton(
@@ -202,7 +332,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
child: const Text('Yes'),
onPressed: () async {
String? deleteResult =
await deleteBusiness(business, jwt);
await deleteBusiness(business.id, jwt);
if (deleteResult != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
+51 -46
View File
@@ -6,8 +6,9 @@ import 'package:flutter/services.dart';
class CreateEditBusiness extends StatefulWidget {
final Business? inputBusiness;
final JobType? clickFromType;
const CreateEditBusiness({super.key, this.inputBusiness});
const CreateEditBusiness({super.key, this.inputBusiness, this.clickFromType});
@override
State<CreateEditBusiness> createState() => _CreateEditBusinessState();
@@ -27,7 +28,6 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
id: 0,
name: 'Business',
description: 'Add details about the business below.',
type: BusinessType.other,
website: '',
contactName: '',
contactEmail: '',
@@ -155,7 +155,9 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
'https://logo.clearbit.com/${business.website}',
errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48,
return getIconFromJobType(
widget.clickFromType ?? JobType.other,
48,
Theme.of(context).colorScheme.onBackground);
}),
),
@@ -239,7 +241,8 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Business Description (required)',
labelText:
'Business Description (required)',
),
validator: (value) {
if (value != null && value.isEmpty) {
@@ -357,46 +360,49 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text('Type of Business',
style: TextStyle(fontSize: 16)),
DropdownMenu<BusinessType>(
initialSelection: business.type,
controller: businessTypeController,
label: const Text('Business Type'),
dropdownMenuEntries: const [
DropdownMenuEntry(
value: BusinessType.food,
label: 'Food Related'),
DropdownMenuEntry(
value: BusinessType.shop,
label: 'Shop'),
DropdownMenuEntry(
value: BusinessType.outdoors,
label: 'Outdoors'),
DropdownMenuEntry(
value: BusinessType.manufacturing,
label: 'Manufacturing'),
DropdownMenuEntry(
value: BusinessType.entertainment,
label: 'Entertainment'),
DropdownMenuEntry(
value: BusinessType.other,
label: 'Other'),
],
onSelected: (inputType) {
business.type = inputType!;
},
),
],
),
),
// Business Type Dropdown
// Padding(
// padding: const EdgeInsets.only(
// left: 8.0, right: 8.0, bottom: 8.0),
// child: Row(
// mainAxisAlignment:
// MainAxisAlignment.spaceBetween,
// children: [
// const Text('Type of Business',
// style: TextStyle(fontSize: 16)),
// DropdownMenu<BusinessType>(
// initialSelection: business.type,
// controller: businessTypeController,
// label: const Text('Business Type'),
// dropdownMenuEntries: const [
// DropdownMenuEntry(
// value: BusinessType.food,
// label: 'Food Related'),
// DropdownMenuEntry(
// value: BusinessType.shop,
// label: 'Shop'),
// DropdownMenuEntry(
// value: BusinessType.outdoors,
// label: 'Outdoors'),
// DropdownMenuEntry(
// value: BusinessType.manufacturing,
// label: 'Manufacturing'),
// DropdownMenuEntry(
// value: BusinessType.entertainment,
// label: 'Entertainment'),
// DropdownMenuEntry(
// value: BusinessType.other,
// label: 'Other'),
// ],
// onSelected: (inputType) {
// business.type = inputType!;
// },
// ),
// ],
// ),
// ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
@@ -409,8 +415,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Contact Information Name',
labelText: 'Contact Information Name',
),
),
),
+42 -31
View File
@@ -16,9 +16,9 @@ bool isBusinessesFiltered = true;
bool _isLoading = false;
class ExportData extends StatefulWidget {
final List<Business> businesses;
final Map<JobType, List<Business>> groupedBusinesses;
const ExportData({super.key, required this.businesses});
const ExportData({super.key, required this.groupedBusinesses});
@override
State<ExportData> createState() => _ExportDataState();
@@ -31,7 +31,7 @@ class _ExportDataState extends State<ExportData> {
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessData();
refreshBusinessDataFuture = fetchBusinessDataOverview();
_isLoading = false;
selectedBusinesses = <Business>{};
}
@@ -39,7 +39,7 @@ class _ExportDataState extends State<ExportData> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _FAB(businesses: widget.businesses),
floatingActionButton: _FAB(groupedBusinesses: widget.groupedBusinesses),
body: CustomScrollView(
slivers: [
SliverAppBar(
@@ -196,7 +196,7 @@ class _ExportDataState extends State<ExportData> {
),
),
BusinessDisplayPanel(
businesses: widget.businesses,
groupedBusinesses: widget.groupedBusinesses,
widescreen: MediaQuery.sizeOf(context).width >= 1000,
selectable: true),
const SliverToBoxAdapter(
@@ -211,15 +211,26 @@ class _ExportDataState extends State<ExportData> {
}
class _FAB extends StatefulWidget {
final List<Business> businesses;
final Map<JobType, List<Business>> groupedBusinesses;
const _FAB({required this.businesses});
const _FAB({required this.groupedBusinesses});
@override
State<_FAB> createState() => _FABState();
}
class _FABState extends State<_FAB> {
List<Business> allBusinesses = [];
@override
void initState() {
super.initState();
for (JobType type in widget.groupedBusinesses.keys) {
allBusinesses.addAll(widget.groupedBusinesses[type]!);
}
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
@@ -273,7 +284,7 @@ class _FABState extends State<_FAB> {
List<pw.TableRow> rows = [];
if (selectedBusinesses.isEmpty) {
selectedBusinesses.addAll(widget.businesses);
selectedBusinesses.addAll(allBusinesses);
isBusinessesFiltered = false;
} else {
isBusinessesFiltered = true;
@@ -284,9 +295,9 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.logo)) {
remainingSpace -= 32;
}
if (dataTypeFilters.contains(DataType.type)) {
remainingSpace -= 56;
}
// if (dataTypeFilters.contains(DataType.type)) {
// remainingSpace -= 56;
// }
if (dataTypeFilters.contains(DataType.contactName)) {
remainingSpace -= 72;
}
@@ -331,9 +342,9 @@ class _FABState extends State<_FAB> {
} else if (dataType == DataType.description) {
width = pw.FixedColumnWidth(descriptionWidth);
columnNum++;
} else if (dataType == DataType.type) {
width = const pw.FixedColumnWidth(56);
columnNum++;
// } else if (dataType == DataType.type) {
// width = const pw.FixedColumnWidth(56);
// columnNum++;
} else if (dataType == DataType.website) {
width = pw.FixedColumnWidth(websiteWidth);
columnNum++;
@@ -394,18 +405,18 @@ class _FABState extends State<_FAB> {
),
padding: const pw.EdgeInsets.all(4.0)));
}
if (dataTypeFilters.contains(DataType.type)) {
data.add(pw.Padding(
child: pw.Text(
business.type.name,
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
}
// if (dataTypeFilters.contains(DataType.type)) {
// data.add(pw.Padding(
// child: pw.Text(
// business.type.name,
// // style: const pw.TextStyle(fontSize: 10)
// ),
// padding: const pw.EdgeInsets.all(4.0)));
// }
if (dataTypeFilters.contains(DataType.website)) {
data.add(pw.Padding(
child: pw.Text(
business.website,
business.website ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@@ -413,7 +424,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactName)) {
data.add(pw.Padding(
child: pw.Text(
business.contactName,
business.contactName ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@@ -421,7 +432,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactEmail)) {
data.add(pw.Padding(
child: pw.Text(
business.contactEmail,
business.contactEmail ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@@ -429,7 +440,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactPhone)) {
data.add(pw.Padding(
child: pw.Text(
business.contactPhone,
business.contactPhone ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@@ -440,7 +451,7 @@ class _FABState extends State<_FAB> {
style = const pw.TextStyle(fontSize: 8);
}
data.add(pw.Padding(
child: pw.Text(business.notes, style: style),
child: pw.Text(business.notes ?? '', style: style),
padding: const pw.EdgeInsets.all(4.0)));
}
@@ -461,10 +472,10 @@ class _FABState extends State<_FAB> {
} else {
rows.add(pw.TableRow(
children: [
pw.Padding(
child: getPwIconFromType(
business.type, 24, PdfColors.black),
padding: const pw.EdgeInsets.all(4.0)),
// pw.Padding(
// child: getPwIconFromType(
// business.type, 24, PdfColors.black),
// padding: const pw.EdgeInsets.all(4.0)),
...data
],
));
+184 -86
View File
@@ -1,4 +1,3 @@
import 'package:collection/collection.dart';
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/pages/business_detail.dart';
import 'package:flutter/material.dart';
@@ -21,7 +20,7 @@ enum DataType {
logo,
name,
description,
type,
// type,
website,
contactName,
contactEmail,
@@ -33,7 +32,7 @@ Map<DataType, int> dataTypeValues = {
DataType.logo: 0,
DataType.name: 1,
DataType.description: 2,
DataType.type: 3,
// DataType.type: 3,
DataType.website: 4,
DataType.contactName: 5,
DataType.contactEmail: 6,
@@ -45,7 +44,7 @@ Map<DataType, String> dataTypeFriendly = {
DataType.logo: 'Logo',
DataType.name: 'Name',
DataType.description: 'Description',
DataType.type: 'Type',
// DataType.type: 'Type',
DataType.website: 'Website',
DataType.contactName: 'Contact Name',
DataType.contactEmail: 'Contact Email',
@@ -71,47 +70,68 @@ enum BusinessType {
other,
}
enum JobType { cashier, server, mechanic, other }
class JobListing {
String? id;
String? businessId;
String name;
String description;
JobType type;
String? wage;
String? link;
JobListing(
{this.id,
this.businessId,
required this.name,
required this.description,
required this.type,
this.wage,
this.link});
}
class Business {
int id;
String name;
String description;
BusinessType type;
String website;
String contactName;
String contactEmail;
String contactPhone;
String notes;
String locationName;
String locationAddress;
String? website;
String? contactName;
String? contactEmail;
String? contactPhone;
String? notes;
String? locationName;
String? locationAddress;
List<JobListing>? listings;
Business({
required this.id,
Business(
{required this.id,
required this.name,
required this.description,
required this.type,
required this.website,
required this.contactName,
required this.contactEmail,
required this.contactPhone,
required this.notes,
required this.locationName,
required this.locationAddress,
});
this.website,
this.contactName,
this.contactEmail,
this.contactPhone,
this.notes,
this.locationName,
this.locationAddress,
this.listings});
factory Business.fromJson(Map<String, dynamic> json) {
bool typeValid = true;
try {
BusinessType.values.byName(json['type']);
} catch (e) {
typeValid = false;
List<JobListing>? listings = [];
for (int i = 0; i < json['listings'].length; i++) {
listings.add(JobListing(
name: json['listings']['name'],
description: json['listings']['description'],
type: json['listings']['type'],
wage: json['listings']['wage'],
link: json['listings']['link']));
}
return Business(
id: json['id'],
name: json['name'],
description: json['description'],
type: typeValid
? BusinessType.values.byName(json['type'])
: BusinessType.other,
website: json['website'],
contactName: json['contactName'],
contactEmail: json['contactEmail'],
@@ -119,7 +139,7 @@ class Business {
notes: json['notes'],
locationName: json['locationName'],
locationAddress: json['locationAddress'],
);
listings: listings);
}
factory Business.copy(Business input) {
@@ -127,7 +147,6 @@ class Business {
id: input.id,
name: input.name,
description: input.description,
type: input.type,
website: input.website,
contactName: input.contactName,
contactEmail: input.contactEmail,
@@ -135,16 +154,16 @@ class Business {
notes: input.notes,
locationName: input.locationName,
locationAddress: input.locationAddress,
);
listings: input.listings);
}
}
Map<BusinessType, List<Business>> groupBusinesses(List<Business> businesses) {
Map<BusinessType, List<Business>> groupedBusinesses =
groupBy<Business, BusinessType>(businesses, (business) => business.type);
return groupedBusinesses;
}
// Map<BusinessType, List<Business>> groupBusinesses(List<Business> businesses) {
// Map<BusinessType, List<Business>> groupedBusinesses =
// groupBy<Business, BusinessType>(businesses, (business) => business.type!);
//
// return groupedBusinesses;
// }
Icon getIconFromType(BusinessType type, double size, Color color) {
switch (type) {
@@ -187,6 +206,35 @@ Icon getIconFromType(BusinessType type, double size, Color color) {
}
}
Icon getIconFromJobType(JobType type, double size, Color color) {
switch (type) {
case JobType.cashier:
return Icon(
Icons.shopping_bag,
size: size,
color: color,
);
case JobType.server:
return Icon(
Icons.restaurant,
size: size,
color: color,
);
case JobType.mechanic:
return Icon(
Icons.construction,
size: size,
color: color,
);
case JobType.other:
return Icon(
Icons.work,
size: size,
color: color,
);
}
}
pw.Icon getPwIconFromType(BusinessType type, double size, PdfColor color) {
switch (type) {
case BusinessType.food:
@@ -204,6 +252,19 @@ pw.Icon getPwIconFromType(BusinessType type, double size, PdfColor color) {
}
}
pw.Icon getPwIconFromJobType(JobType type, double size, PdfColor color) {
switch (type) {
case JobType.cashier:
return pw.Icon(const pw.IconData(0xf1cc), size: size, color: color);
case JobType.server:
return pw.Icon(const pw.IconData(0xe56c), size: size, color: color);
case JobType.mechanic:
return pw.Icon(const pw.IconData(0xea3c), size: size, color: color);
case JobType.other:
return pw.Icon(const pw.IconData(0xe8f9), size: size, color: color);
}
}
Text getNameFromType(BusinessType type, Color color) {
switch (type) {
case BusinessType.food:
@@ -221,6 +282,19 @@ Text getNameFromType(BusinessType type, Color color) {
}
}
Text getNameFromJobType(JobType type, Color color) {
switch (type) {
case JobType.cashier:
return Text('Cashier', style: TextStyle(color: color));
case JobType.server:
return Text('Server', style: TextStyle(color: color));
case JobType.mechanic:
return Text('Mechanic', style: TextStyle(color: color));
case JobType.other:
return Text('Other', style: TextStyle(color: color));
}
}
Icon getIconFromThemeMode(ThemeMode theme) {
switch (theme) {
case ThemeMode.dark:
@@ -233,13 +307,13 @@ Icon getIconFromThemeMode(ThemeMode theme) {
}
class BusinessDisplayPanel extends StatefulWidget {
final List<Business> businesses;
final Map<JobType, List<Business>> groupedBusinesses;
final bool widescreen;
final bool selectable;
const BusinessDisplayPanel(
{super.key,
required this.businesses,
required this.groupedBusinesses,
required this.widescreen,
required this.selectable});
@@ -253,40 +327,42 @@ class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
@override
Widget build(BuildContext context) {
List<BusinessHeader> headers = [];
List<Business> filteredBusinesses = [];
for (var business in widget.businesses) {
if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) {
filteredBusinesses.add(business);
}
}
var groupedBusinesses = groupBusinesses(filteredBusinesses);
var businessTypes = groupedBusinesses.keys.toList();
// List<Business> filteredBusinesses = [];
// for (var business in widget.groupedBusinesses.) {
// if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) {
// filteredBusinesses.add(business);
// }
// }
for (var i = 0; i < businessTypes.length; i++) {
if (filters.contains(businessTypes[i])) {
if (filters.isNotEmpty) {
isFiltered = true;
}
}
// for (var i = 0; i < businessTypes.length; i++) {
// if (filters.contains(businessTypes[i])) {
// isFiltered = true;
// }
// }
if (isFiltered) {
for (var i = 0; i < businessTypes.length; i++) {
if (filters.contains(businessTypes[i])) {
for (JobType jobType in widget.groupedBusinesses.keys) {
if (filters.contains(jobType)) {
headers.add(BusinessHeader(
type: businessTypes[i],
type: jobType,
widescreen: widget.widescreen,
selectable: widget.selectable,
selectedBusinesses: selectedBusinesses,
businesses: groupedBusinesses[businessTypes[i]]!));
businesses: widget.groupedBusinesses[jobType]!));
}
}
} else {
for (var i = 0; i < businessTypes.length; i++) {
for (JobType jobType in widget.groupedBusinesses.keys) {
headers.add(BusinessHeader(
type: businessTypes[i],
type: jobType,
widescreen: widget.widescreen,
selectable: widget.selectable,
selectedBusinesses: selectedBusinesses,
businesses: groupedBusinesses[businessTypes[i]]!));
businesses: widget.groupedBusinesses[jobType]!));
}
}
headers.sort((a, b) => a.type.index.compareTo(b.type.index));
@@ -295,7 +371,7 @@ class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
}
class BusinessHeader extends StatefulWidget {
final BusinessType type;
final JobType type;
final List<Business> businesses;
final Set<Business> selectedBusinesses;
final bool widescreen;
@@ -343,10 +419,10 @@ class _BusinessHeaderState extends State<BusinessHeader> {
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromType(
child: getIconFromJobType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary),
),
getNameFromType(
getNameFromJobType(
widget.type, Theme.of(context).colorScheme.onPrimary),
],
),
@@ -376,10 +452,11 @@ class _BusinessHeaderState extends State<BusinessHeader> {
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromType(
child: getIconFromJobType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary),
),
getNameFromType(widget.type, Theme.of(context).colorScheme.onPrimary),
getNameFromJobType(
widget.type, Theme.of(context).colorScheme.onPrimary),
],
);
}
@@ -404,6 +481,7 @@ class _BusinessHeaderState extends State<BusinessHeader> {
selectable: selectable,
widescreen: widescreen,
callback: refresh,
type: widget.type,
);
},
),
@@ -418,6 +496,7 @@ class _BusinessHeaderState extends State<BusinessHeader> {
selectable: selectable,
widescreen: widescreen,
callback: refresh,
type: widget.type,
);
},
),
@@ -431,13 +510,15 @@ class BusinessCard extends StatefulWidget {
final bool widescreen;
final bool selectable;
final Function callback;
final JobType type;
const BusinessCard(
{super.key,
required this.business,
required this.widescreen,
required this.selectable,
required this.callback});
required this.callback,
required this.type});
@override
State<BusinessCard> createState() => _BusinessCardState();
@@ -447,27 +528,31 @@ class _BusinessCardState extends State<BusinessCard> {
@override
Widget build(BuildContext context) {
if (widget.widescreen) {
return _businessTile(widget.business, widget.selectable);
return _businessTile(widget.business, widget.selectable, widget.type);
} else {
return _businessListItem(
widget.business, widget.selectable, widget.callback);
widget.business, widget.selectable, widget.callback, widget.type);
}
}
Widget _businessTile(Business business, bool selectable) {
Widget _businessTile(Business business, bool selectable, JobType type) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(inputBusiness: business)));
builder: (context) => BusinessDetail(
id: business.id,
name: business.name,
clickFromType: type,
)));
},
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getTileRow(business, selectable, widget.callback),
_getTileRow(business, selectable, widget.callback, type),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
@@ -490,7 +575,8 @@ class _BusinessCardState extends State<BusinessCard> {
Uri.parse('https://${business.website}'));
},
),
if (business.locationName.isNotEmpty)
if ((business.locationName != null) &&
(business.locationName != ''))
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
@@ -498,7 +584,8 @@ class _BusinessCardState extends State<BusinessCard> {
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
if (business.contactPhone.isNotEmpty)
if ((business.contactPhone != null) &&
(business.contactPhone != ''))
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
@@ -509,10 +596,14 @@ class _BusinessCardState extends State<BusinessCard> {
backgroundColor: Theme.of(context)
.colorScheme
.background,
title: Text(business.contactName.isEmpty
title: Text((business.contactName ==
null ||
business.contactName == '')
? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'),
content: Text(business.contactName.isEmpty
content: Text((business.contactName ==
null ||
business.contactName == '')
? 'Would you like to call or text ${business.name}?'
: 'Would you like to call or text ${business.contactName}?'),
actions: [
@@ -535,7 +626,8 @@ class _BusinessCardState extends State<BusinessCard> {
});
},
),
if (business.contactEmail.isNotEmpty)
if ((business.contactEmail != null) &&
(business.contactEmail != ''))
IconButton(
icon: const Icon(Icons.email),
onPressed: () {
@@ -553,7 +645,8 @@ class _BusinessCardState extends State<BusinessCard> {
);
}
Widget _getTileRow(Business business, bool selectable, Function callback) {
Widget _getTileRow(
Business business, bool selectable, Function callback, JobType type) {
if (selectable) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -565,8 +658,8 @@ class _BusinessCardState extends State<BusinessCard> {
child: Image.network('$apiAddress/logos/${business.id}',
height: 48, width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(
business.type, 48, Theme.of(context).colorScheme.onSurface);
return getIconFromJobType(
type, 48, Theme.of(context).colorScheme.onSurface);
}),
),
),
@@ -598,8 +691,8 @@ class _BusinessCardState extends State<BusinessCard> {
child: Image.network('$apiAddress/logos/${business.id}',
height: 48, width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48,
Theme.of(context).colorScheme.onSurface);
return getIconFromJobType(
type, 48, Theme.of(context).colorScheme.onSurface);
}),
)),
Flexible(
@@ -620,7 +713,7 @@ class _BusinessCardState extends State<BusinessCard> {
}
Widget _businessListItem(
Business business, bool selectable, Function callback) {
Business business, bool selectable, Function callback, JobType type) {
return Card(
child: ListTile(
leading: ClipRRect(
@@ -628,8 +721,8 @@ class _BusinessCardState extends State<BusinessCard> {
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(
business.type, 24, Theme.of(context).colorScheme.onSurface);
return getIconFromJobType(
type, 24, Theme.of(context).colorScheme.onSurface);
})),
title: Text(business.name),
subtitle: Text(business.description,
@@ -637,7 +730,11 @@ class _BusinessCardState extends State<BusinessCard> {
trailing: _getCheckbox(selectable, callback),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(inputBusiness: business)));
builder: (context) => BusinessDetail(
id: business.id,
name: business.name,
clickFromType: type,
)));
},
),
);
@@ -688,7 +785,8 @@ class _FilterChipsState extends State<FilterChips> {
showCheckmark: false,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
label: getNameFromType(type, Theme.of(context).colorScheme.onSurface),
label:
getNameFromType(type, Theme.of(context).colorScheme.onSurface),
selected: selectedChips.contains(type),
onSelected: (bool selected) {
setState(() {
+1 -1
View File
@@ -41,7 +41,7 @@ endif()
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE -Wall) # removed -Werror to avoid error from libraries on unused vars
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
@@ -7,12 +7,16 @@
#include "generated_plugin_registrant.h"
#include <printing/printing_plugin.h>
#include <rive_common/rive_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) printing_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
printing_plugin_register_with_registrar(printing_registrar);
g_autoptr(FlPluginRegistrar) rive_common_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RivePlugin");
rive_plugin_register_with_registrar(rive_common_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
printing
rive_common
url_launcher_linux
)
+56 -56
View File
@@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.4.10"
version: "3.6.1"
async:
dependency: transitive
description:
@@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: barcode
sha256: "91b143666f7bb13636f716b6d4e412e372ab15ff7969799af8c9e30a382e9385"
sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
url: "https://pub.dev"
source: hosted
version: "2.2.6"
version: "2.2.8"
bidi:
dependency: transitive
description:
@@ -93,18 +93,18 @@ packages:
dependency: "direct main"
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "1.0.8"
dart_jsonwebtoken:
dependency: "direct main"
description:
name: dart_jsonwebtoken
sha256: "40dc3a4788c02a44bc97ea0c8c4a078ae58c9a45acc2312ee6a689b0e8f5b5b9"
sha256: "346e9a21e4bf6e6a431e19ece00ebb2e3668e1e339cabdf6f46d18d88692a848"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.14.0"
ed25519_edwards:
dependency: transitive
description:
@@ -204,10 +204,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev"
source: hosted
version: "4.1.7"
version: "4.2.0"
js:
dependency: transitive
description:
@@ -220,26 +220,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.1"
lints:
dependency: transitive
description:
@@ -268,10 +268,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.12.0"
open_filex:
dependency: "direct main"
description:
@@ -300,26 +300,26 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.2.5"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
path_provider_linux:
dependency: transitive
description:
@@ -356,10 +356,10 @@ packages:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: "9c3ca36e5000c9682d52bbdc486867ba7c5ee4403d1a5d6d03ed72157753377b"
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.3"
version: "1.0.4"
petitparser:
dependency: transitive
description:
@@ -388,10 +388,10 @@ packages:
dependency: transitive
description:
name: pointycastle
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.7.4"
version: "3.9.1"
printing:
dependency: "direct main"
description:
@@ -412,42 +412,42 @@ packages:
dependency: "direct main"
description:
name: rive
sha256: ec44b6cf7341e21727c4b0e762f4ac82f9a45f7e52df3ebad2d1289a726fbaaf
sha256: "03bae056e67a5c98f9523c34f8ea63c15b323426a7e8d76d7ebb93066d4df6dc"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
version: "0.13.5"
rive_common:
dependency: transitive
description:
name: rive_common
sha256: "0f070bc0e764c570abd8b34d744ef30d1292bd4051f2e0951bbda755875fce6a"
sha256: "3fe76ba4680787741688ee393e47b63417e8643816795e4eac01021683af1d84"
url: "https://pub.dev"
source: hosted
version: "0.3.3"
version: "0.4.9"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.3"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
version: "2.4.0"
shared_preferences_linux:
dependency: transitive
description:
@@ -537,10 +537,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.0"
typed_data:
dependency: transitive
description:
@@ -553,26 +553,26 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
url: "https://pub.dev"
source: hosted
version: "6.2.5"
version: "6.3.0"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.3"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
url: "https://pub.dev"
source: hosted
version: "6.2.5"
version: "6.3.0"
url_launcher_linux:
dependency: transitive
description:
@@ -585,10 +585,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.2.0"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -601,10 +601,10 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d"
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.1"
url_launcher_windows:
dependency: transitive
description:
@@ -633,10 +633,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
version: "14.2.1"
web:
dependency: transitive
description:
@@ -649,10 +649,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480"
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev"
source: hosted
version: "5.3.0"
version: "5.5.1"
xdg_directories:
dependency: transitive
description:
@@ -670,5 +670,5 @@ packages:
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"