From 981312b15f3b0550bbd6c5758d8404a8dbfb211b Mon Sep 17 00:00:00 2001 From: dan63047 Date: Tue, 16 May 2023 23:07:18 +0300 Subject: [PATCH] First implementation of sqlite db, idk what i'm doing --- lib/data_objects/tetrio.dart | 85 ++++++++-- lib/main.dart | 9 +- lib/services/tetrio_crud.dart | 107 +++++++++++++ lib/views/main_view.dart | 9 +- macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 147 +++++++++++++++++- pubspec.yaml | 4 + 7 files changed, 342 insertions(+), 23 deletions(-) create mode 100644 lib/services/tetrio_crud.dart diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index 379ace7..9233dd8 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -15,6 +15,7 @@ Duration doubleMillisecondsToDuration(double value) { class TetrioPlayer { late String userId; late String username; + late DateTime state; late String role; int? avatarRevision; int? bannerRevision; @@ -42,6 +43,7 @@ class TetrioPlayer { required this.userId, required this.username, required this.role, + required this.state, this.registrationTime, required this.badges, this.bio, @@ -60,15 +62,16 @@ class TetrioPlayer { this.zen, }); - double getLevel() { + double get level{ return pow((xp / 500), 0.6) + (xp / (5000 + (max(0, xp - 4 * pow(10, 6)) / 5000))) + 1; } - TetrioPlayer.fromJson(Map json) { + TetrioPlayer.fromJson(Map json, DateTime stateTime) { userId = json['_id']; username = json['username']; + state = stateTime; role = json['role']; registrationTime = json['ts'] != null ? DateTime.parse(json['ts']) : null; if (json['badges'] != null) { @@ -91,24 +94,27 @@ class TetrioPlayer { distinguishment = json['distinguishment'] != null ? Distinguishment.fromJson(json['distinguishment']) : null; - var url = Uri.https('ch.tetr.io', 'api/users/$userId/records'); - Future response = http.get(url); - response.then((value) { - if (value.statusCode == 200) { - if(jsonDecode(value.body)['data']['records']['40l']['record'] != null){ - sprint.add(RecordSingle.fromJson(jsonDecode(value.body)['data']['records']['40l']['record'])); - } - if(jsonDecode(value.body)['data']['records']['blitz']['record'] != null){ - blitz.add(RecordSingle.fromJson(jsonDecode(value.body)['data']['records']['blitz']['record'])); - } - zen = TetrioZen.fromJson(jsonDecode(value.body)['data']['zen']); - } else { - throw Exception('Failed to fetch player'); - } - }); friendCount = json['friend_count']; } + Future getRecords() async { + var url = Uri.https('ch.tetr.io', 'api/users/$userId/records'); + final response = await http.get(url); + if (response.statusCode == 200) { + if(jsonDecode(response.body)['data']['records']['40l']['record'] != null){ + sprint.add(RecordSingle.fromJson(jsonDecode(response.body)['data']['records']['40l']['record'])); + } + if(jsonDecode(response.body)['data']['records']['blitz']['record'] != null){ + blitz.add(RecordSingle.fromJson(jsonDecode(response.body)['data']['records']['blitz']['record'])); + } + zen = TetrioZen.fromJson(jsonDecode(response.body)['data']['zen']); + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + throw Exception('Failed to fetch player'); + } + } + Map toJson() { final Map data = {}; data['_id'] = userId; @@ -124,6 +130,7 @@ class TetrioPlayer { data['supporter_tier'] = supporterTier; data['verified'] = verified; data['league'] = tlSeason1.toJson(); + data['distinguishment'] = distinguishment?.toJson(); data['avatar_revision'] = avatarRevision; data['banner_revision'] = bannerRevision; data['bio'] = bio; @@ -131,6 +138,39 @@ class TetrioPlayer { data['friend_count'] = friendCount; return data; } + + bool isSameState(TetrioPlayer other){ + if (userId != other.userId) return false; + if (username != other.username) return false; + if (role != other.role) return false; + if (badges != other.badges) return false; + if (bio != other.bio) return false; + if (country != other.country) return false; + if (friendCount != other.friendCount) return false; + if (gamesPlayed != other.gamesPlayed) return false; + if (gamesWon != other.gamesWon) return false; + if (gameTime != other.gameTime) return false; + if (xp != other.xp) return false; + if (supporterTier != other.supporterTier) return false; + if (verified != other.verified) return false; + if (badstanding != other.badstanding) return false; + if (bot != other.bot) return false; + if (connections != other.connections) return false; + if (tlSeason1 != other.tlSeason1) return false; + if (distinguishment != other.distinguishment) return false; + return true; + } + + @override + String toString(){ + return "$username ($userId)"; + } + + @override + int get hashCode => state.hashCode; + + @override + bool operator ==(covariant TetrioPlayer other) => (userId == other.userId); } class Badge { @@ -153,6 +193,17 @@ class Badge { data['ts'] = ts; return data; } + + @override + String toString(){ + return "Badge $label ($badgeId)"; + } + + @override + int get hashCode => badgeId.hashCode; + + @override + bool operator ==(covariant Badge other) => badgeId == other.badgeId; } class Connections { diff --git a/lib/main.dart b/lib/main.dart index 23aed5f..a35400a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:tetra_stats/views/main_view.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -void main() => runApp(MaterialApp( - home: MainView(), - )); +void main() { + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + runApp(MaterialApp(home: MainView())); +} diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart new file mode 100644 index 0000000..165d47a --- /dev/null +++ b/lib/services/tetrio_crud.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:sqflite/sqflite.dart'; +import 'package:path_provider/path_provider.dart' show MissingPlatformDirectoryException, getApplicationDocumentsDirectory; +import 'package:path/path.dart' show join; +import 'package:tetra_stats/data_objects/tetrio.dart'; + +const String dbName = "TetraStats.db"; +const String tetrioUsersTable = "tetrioUsers"; +const String idCol = "id"; +const String nickCol = "nickname"; +const String statesCol = "jsonStates"; +const String createTetrioUsersTable = ''' + CREATE TABLE IF NOT EXISTS "tetrioUsers" ( + "id" TEXT UNIQUE, + "nickname" TEXT, + "jsonStates" TEXT, + PRIMARY KEY("id") + );'''; + +class DatabaseAlreadyOpen implements Exception {} +class DatabaseIsNotOpen implements Exception {} +class UnableToGetDocuments implements Exception {} +class CouldNotDeletePlayer implements Exception{} +class TetrioPlayerAlreadyExist implements Exception{} +class TetrioPlayerNotExist implements Exception{} + +class TetrioService{ + Database? _db; + Map> _players = {}; + final _tetrioStreamController = StreamController>>.broadcast(); + + Database _getDatabaseOrThrow(){ + final db = _db; + if(db == null){ + throw DatabaseIsNotOpen(); + }else{ + return db; + } + } + + Future open() async{ + if (_db != null){ + throw DatabaseAlreadyOpen(); + } + try{ + final docsPath = await getApplicationDocumentsDirectory(); + final dbPath = join(docsPath.path, dbName); + final db = await openDatabase(dbPath); + _db = db; + await db.execute(createTetrioUsersTable); + } on MissingPlatformDirectoryException { + throw UnableToGetDocuments(); + } + } + + Future close() async{ + final db = _db; + if(db == null){ + throw DatabaseIsNotOpen(); + }else{ + await db.close(); + _db = null; + } + } + + Future _cachePlayers() async{ + //final allPlayers = await getAllPlayers(); + } + + Future deleteTetrioPlayer({required String id}) async{ + final db = _getDatabaseOrThrow(); + final deletedPlayer = await db.delete(tetrioUsersTable, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); + if (deletedPlayer != 1){ + throw CouldNotDeletePlayer(); + } + } + + Future storeUser({required TetrioPlayer tetrioPlayer}) async{ + final db = _getDatabaseOrThrow(); + final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [tetrioPlayer.userId.toLowerCase()]); + if(results.isNotEmpty){ + throw TetrioPlayerAlreadyExist(); + } + final Map statesJson = {tetrioPlayer.state.toString(): tetrioPlayer.toJson().toString()}; + db.insert(tetrioUsersTable, { + idCol: tetrioPlayer.userId, + nickCol: tetrioPlayer.username, + statesCol: statesJson + }); + } + + Future> getUser({required String id}) async{ + final db = _getDatabaseOrThrow(); + List states = []; + final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]); + if(results.isEmpty){ + throw TetrioPlayerNotExist(); + }else{ + dynamic rawStates = results.first['jsonStates'] as String; + rawStates = json.decode(rawStates); + rawStates.forEach((k,v) => states.add(TetrioPlayer.fromJson(v, DateTime.now()))); + return states; + } + } +} \ No newline at end of file diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index d76f1c1..426244f 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/services/tetrio_crud.dart'; String _searchFor = ""; late TetrioPlayer me; +TetrioService teto = TetrioService(); class MainView extends StatefulWidget { const MainView({Key? key}) : super(key: key); @@ -16,13 +18,14 @@ class MainView extends StatefulWidget { class _MainViewState extends State { Future fetchTetrioPlayer(String user) async { var url = Uri.https('ch.tetr.io', 'api/users/$user'); + teto.open(); final response = await http.get(url); // final response = await http.get(Uri.parse('https://ch.tetr.io/')); if (response.statusCode == 200) { // If the server did return a 200 OK response, // then parse the JSON. - return TetrioPlayer.fromJson(jsonDecode(response.body)['data']['user']); + return TetrioPlayer.fromJson(jsonDecode(response.body)['data']['user'], DateTime.fromMillisecondsSinceEpoch(jsonDecode(response.body)['cache']['cached_at'], isUtc: true)); } else { // If the server did not return a 200 OK response, // then throw an exception. @@ -73,13 +76,15 @@ class _MainViewState extends State { future: me, builder: (context, snapshot) { if (snapshot.hasData) { + snapshot.data!.getRecords(); + teto.getUser(id: snapshot.data!.userId); return Flexible( child: Column(children: [ Text(snapshot.data!.username.toString()), Text(snapshot.data!.userId.toString()), Text(snapshot.data!.role.toString()), Text( - "Level ${snapshot.data!.getLevel().toStringAsFixed(2)} (${snapshot.data!.xp} XP)"), + "Level ${snapshot.data!.level.toStringAsFixed(2)} (${snapshot.data!.xp} XP)"), Text("Registered ${snapshot.data!.registrationTime}"), Text("Bio: ${snapshot.data!.bio}", softWrap: true), Text("Country: ${snapshot.data!.country}"), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..2bfe7e4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import path_provider_foundation +import sqflite func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index a005d93..20cc8a2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" flutter: dependency: "direct main" description: flutter @@ -132,13 +148,85 @@ packages: source: hosted version: "1.8.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b url: "https://pub.dev" source: hosted version: "1.8.2" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + url: "https://pub.dev" + source: hosted + version: "2.0.15" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.dev" + source: hosted + version: "2.0.27" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + url: "https://pub.dev" + source: hosted + version: "2.1.10" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" sky_engine: dependency: transitive description: flutter @@ -152,6 +240,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: "3a82c9a216b46b88617e3714dd74227eaca20c501c4abcc213e56db26b9caa00" + url: "https://pub.dev" + source: hosted + version: "2.2.8+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555 + url: "https://pub.dev" + source: hosted + version: "2.4.5" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: f86de82d37403af491b21920a696b19f01465b596f545d1acd4d29a0a72418ad + url: "https://pub.dev" + source: hosted + version: "2.2.5" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "2cef47b59d310e56f8275b13734ee80a9cf4a48a43172020cb55a620121fbf66" + url: "https://pub.dev" + source: hosted + version: "1.11.1" stack_trace: dependency: transitive description: @@ -176,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" term_glyph: dependency: transitive description: @@ -208,5 +336,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + win32: + dependency: transitive + description: + name: win32 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + url: "https://pub.dev" + source: hosted + version: "4.1.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" + source: hosted + version: "1.0.0" sdks: dart: ">=2.19.6 <3.0.0" + flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index 41d691d..b756c5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,10 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + sqflite: ^2.2.8+2 + sqflite_common_ffi: any + path_provider: ^2.0.15 + path: ^1.8.2 dev_dependencies: flutter_test: