diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a2d02f..27f79b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,6 +121,21 @@ jobs: name: tokhub-macos.tar.gz path: tokhub-macos.tar.gz + web-build: + runs-on: windows-2022 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + - run: ./tools/prepare-web + - run: flutter build web + windows-build: runs-on: windows-2022 defaults: diff --git a/.metadata b/.metadata index c2aa44b..9a613f0 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "603104015dd692ea3403755b55d07813d5cf8965" + revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: android - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: ios - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: linux - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: macos - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: windows - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 + create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a + base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section diff --git a/.restyled.yaml b/.restyled.yaml new file mode 100644 index 0000000..c4614d8 --- /dev/null +++ b/.restyled.yaml @@ -0,0 +1,8 @@ +--- +exclude: + - "LICENSE" + # Generated files. + - "linux/**/*.cc" + - "linux/**/*.h" + - "windows/**/*.cc" + - "windows/**/*.h" diff --git a/android/app/build.gradle b/android/app/build.gradle index 12c5231..d5e6ffc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,7 +8,7 @@ plugins { android { namespace = "com.example.tokhub" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2e81024..696d9b2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ loadDataObject>( T Function(Map json) fromJson) async { - final dir = await getApplicationDocumentsDirectory(); - final file = File('${dir.path}/TokHub/${T.toString()}.json'); - debugPrint('Loading data object from ${file.path}'); - if (!await file.exists()) { + try { + final dir = await getApplicationDocumentsDirectory(); + final file = File('${dir.path}/TokHub/${T.toString()}.json'); + debugPrint('Loading data object from ${file.path}'); + if (!await file.exists()) { + return null; + } + final text = await file.readAsString(); + return fromJson(jsonDecode(text)); + } catch (e, stackTrace) { + _logger.e('Error loading data object: $e', stackTrace); return null; } - final text = await file.readAsString(); - return fromJson(jsonDecode(text)); } Future saveDataObject(DataObject object) async { - final dir = await getApplicationDocumentsDirectory(); - final file = File('${dir.path}/TokHub/${T.toString()}.json'); - debugPrint('Saving data object to ${file.path}'); - await file.create(recursive: true); - await file.writeAsString(jsonEncode(object.toJson())); + try { + final dir = await getApplicationDocumentsDirectory(); + final file = File('${dir.path}/TokHub/${T.toString()}.json'); + debugPrint('Saving data object to ${file.path}'); + await file.create(recursive: true); + await file.writeAsString(jsonEncode(object.toJson())); + } catch (e, stackTrace) { + _logger.e('Error saving data object: $e', stackTrace); + } } abstract class DataObject { diff --git a/lib/db/database.dart b/lib/db/database.dart new file mode 100644 index 0000000..ac9a259 --- /dev/null +++ b/lib/db/database.dart @@ -0,0 +1,30 @@ +import 'package:drift/drift.dart'; +import 'package:tokhub/models/check_status.dart'; +import 'package:tokhub/models/github.dart'; +import 'package:tokhub/models/json_enum.dart'; + +part 'database.g.dart'; + +@DriftDatabase(tables: [ + MinimalCheckRun, + MinimalCommitStatus, + MinimalPullRequest, + MinimalRepository, + MinimalUser, +]) +final class Database extends _$Database { + Database(super.e); + + @override + int get schemaVersion => 1; + + Future clear() async { + await transaction(() async { + await delete(minimalCheckRun).go(); + await delete(minimalCommitStatus).go(); + await delete(minimalPullRequest).go(); + await delete(minimalRepository).go(); + await delete(minimalUser).go(); + }); + } +} diff --git a/lib/db/native.dart b/lib/db/native.dart new file mode 100644 index 0000000..921b2f6 --- /dev/null +++ b/lib/db/native.dart @@ -0,0 +1,12 @@ +import 'dart:io'; + +import 'package:tokhub/db/database.dart'; +import 'package:drift/native.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +Future constructDb() async { + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(path.join(dbFolder.path, 'TokHub', 'db.sqlite')); + return Database(NativeDatabase(file)); +} diff --git a/lib/db/shared.dart b/lib/db/shared.dart new file mode 100644 index 0000000..64dd575 --- /dev/null +++ b/lib/db/shared.dart @@ -0,0 +1,3 @@ +export 'unsupported.dart' + if (dart.library.html) 'web.dart' + if (dart.library.ffi) 'native.dart'; diff --git a/lib/db/unsupported.dart b/lib/db/unsupported.dart new file mode 100644 index 0000000..b3be8d8 --- /dev/null +++ b/lib/db/unsupported.dart @@ -0,0 +1,3 @@ +import 'package:tokhub/db/database.dart'; + +Future constructDb() => throw UnimplementedError(); diff --git a/lib/db/web.dart b/lib/db/web.dart new file mode 100644 index 0000000..4a16e59 --- /dev/null +++ b/lib/db/web.dart @@ -0,0 +1,11 @@ +import 'package:tokhub/db/database.dart'; +import 'package:drift/wasm.dart'; + +Future constructDb() async { + final db = await WasmDatabase.open( + databaseName: 'db', + sqlite3Uri: Uri(path: 'sqlite3.wasm'), + driftWorkerUri: Uri(path: 'drift_worker.js'), + ); + return Database(db.resolvedExecutor); +} diff --git a/lib/logger.dart b/lib/logger.dart index 6b268b8..c7dca15 100644 --- a/lib/logger.dart +++ b/lib/logger.dart @@ -2,6 +2,12 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:circular_buffer/circular_buffer.dart'; +String _callerFileLine() { + final stack = StackTrace.current.toString().split('\n'); + final caller = stack.firstWhere((line) => !line.contains('logger.dart')); + return caller.split(' ').last; +} + final class Logger { static const _bufferSize = 1000; static final CircularBuffer _log = CircularBuffer(_bufferSize); @@ -35,7 +41,8 @@ final class Logger { void _logLine(LogLevel level, String text, [StackTrace? stackTrace]) { if (level == LogLevel.verbose && !verbose) return; - final line = '${level.name[0].toUpperCase()} $tags $text'; + final line = + '${_callerFileLine()}: ${level.name[0].toUpperCase()} $tags $text'; if (stackTrace != null) { debugPrintStack(stackTrace: stackTrace, label: line); _log.add(LogLine(clock.now(), level, tags, text, stackTrace)); @@ -44,6 +51,18 @@ final class Logger { _log.add(LogLine(clock.now(), level, tags, text)); } } + + T Function(A) catching(T Function(A) f) { + return (A a) { + try { + v('Calling $f with $a'); + return f(a); + } catch (exn, stackTrace) { + e('Caught exception $exn', stackTrace); + rethrow; + } + }; + } } enum LogLevel { verbose, debug, info, warning, error } diff --git a/lib/models/check_status.dart b/lib/models/check_status.dart index 2f3adf9..60117ba 100644 --- a/lib/models/check_status.dart +++ b/lib/models/check_status.dart @@ -1,4 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:tokhub/models/json_enum.dart'; + +enum CheckRunConclusion implements JsonEnum { + success('success'), + failure('failure'), + neutral('neutral'), + cancelled('cancelled'), + timedOut('timed_out'), + skipped('skipped'), + actionRequired('action_required'); + + @override + final String string; + + const CheckRunConclusion(this.string); +} final class CheckStatus { final String name; @@ -14,7 +30,7 @@ final class CheckStatus { }); } -enum CheckStatusConclusion { +enum CheckStatusConclusion implements JsonEnum { unknown('unknown', 'Unknown', Icons.warning, Colors.red), loading('loading', 'Loading', Icons.timer, Colors.blue), success('success', 'Successful in', Icons.check_circle, Colors.green), @@ -28,6 +44,7 @@ enum CheckStatusConclusion { failure('failure', 'Failing after', Icons.error_outline, Colors.red), empty('empty', 'Unknown after', Icons.help, Colors.grey); + @override final String string; final String title; final IconData icon; diff --git a/lib/models/github.dart b/lib/models/github.dart index 7a60863..5a24a79 100644 --- a/lib/models/github.dart +++ b/lib/models/github.dart @@ -1,273 +1,150 @@ import 'dart:convert'; +import 'package:drift/drift.dart'; import 'package:github/github.dart'; -import 'package:objectbox/objectbox.dart'; -import 'package:tokhub/logger.dart'; +import 'package:tokhub/db/database.dart'; import 'package:tokhub/models/check_status.dart'; - -const _logger = Logger(['GitHubModels']); - -@Entity() -final class MinimalCheckRun { - @Id(assignable: true) - int id; - String name; - @Index() - String headSha; - String detailsUrl; - @Property(type: PropertyType.date) - DateTime? startedAt; - @Property(type: PropertyType.date) - DateTime? completedAt; - - @Transient() - CheckStatusConclusion conclusion; - MinimalCheckRun() - : id = 0, - name = '', - headSha = '', - detailsUrl = '', - conclusion = CheckStatusConclusion.unknown, - startedAt = null, - completedAt = null; - factory MinimalCheckRun.fromJson(Map json) { - return MinimalCheckRun._( - id: json['id'] as int, - name: json['name'] as String, - headSha: json['head_sha'] as String, - detailsUrl: json['details_url'] as String, - conclusion: CheckStatusConclusion.values.firstWhere( - (e) => e.string == json['conclusion'], - orElse: () { - _logger.w( - 'Unknown check run conclusion in ${json['name']}: ${json['conclusion']}'); - return CheckStatusConclusion.unknown; - }, - ), - startedAt: json['started_at'] == null - ? null - : DateTime.parse(json['started_at'] as String), - completedAt: json['completed_at'] == null - ? null - : DateTime.parse(json['completed_at'] as String), - ); - } - - MinimalCheckRun._({ - required this.id, - required this.name, - required this.headSha, - required this.detailsUrl, - required this.conclusion, - required this.startedAt, - required this.completedAt, - }); - - String get conclusionStored => conclusion.name; - - set conclusionStored(String value) { - conclusion = - CheckStatusConclusion.values.firstWhere((e) => e.name == value); - } +import 'package:tokhub/models/json_enum.dart'; + +class MinimalCheckRun extends Table { + @JsonKey('completed_at') + DateTimeColumn get completedAt => dateTime().nullable()(); + TextColumn get conclusion => text() + .nullable() + .map(const JsonEnumConverter(CheckStatusConclusion.values))(); + @JsonKey('details_url') + TextColumn get detailsUrl => text()(); + @JsonKey('head_sha') + TextColumn get headSha => text()(); + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + TextColumn get repoSlug => text().nullable()(); + @JsonKey('started_at') + DateTimeColumn get startedAt => dateTime().nullable()(); } -@Entity() -final class MinimalPullRequest { - @Id(assignable: true) - int id; - int number; - bool draft; - String title; - String mergeableState; - String htmlUrl; - - @Transient() - MinimalPullRequestHead head; - final ToOne user; - final ToOne repo; - - @Property(type: PropertyType.date) - DateTime? updatedAt; - MinimalPullRequest() - : id = 0, - number = 0, - draft = false, - title = '', - mergeableState = '', - htmlUrl = '', - head = const MinimalPullRequestHead.empty(), - user = ToOne(), - repo = ToOne(), - updatedAt = null; - - factory MinimalPullRequest.fromJson(Map json) { - return MinimalPullRequest._( - id: json['id'] as int, - number: json['number'] as int, - draft: json['draft'] as bool, - title: json['title'] as String, - mergeableState: json['mergeable_state'] as String, - htmlUrl: json['html_url'] as String, - head: MinimalPullRequestHead.fromJson(json['head']), - user: ToOne(target: MinimalUser.fromJson(json['user'])), - repo: ToOne(target: MinimalRepository.fromJson(json['base']['repo'])), - updatedAt: DateTime.parse(json['updated_at'] as String), - ); - } - - MinimalPullRequest._({ - required this.id, - required this.number, - required this.draft, - required this.title, - required this.mergeableState, - required this.htmlUrl, - required this.head, - required this.user, - required this.repo, - required this.updatedAt, - }); - - String get headStored => jsonEncode(head.toJson()); +class MinimalCommitStatus extends Table { + TextColumn get context => text()(); + TextColumn get description => text().nullable()(); + TextColumn get headSha => text().nullable()(); + IntColumn get id => integer().autoIncrement()(); + TextColumn get repoSlug => text().nullable()(); + TextColumn get state => text()(); + TextColumn get targetUrl => text().nullable()(); +} - set headStored(String value) => - head = MinimalPullRequestHead.fromJson(jsonDecode(value)); +class MinimalPullRequest extends Table { + TextColumn get base => text().map(const MinimalPullRequestHeadConverter())(); + BoolColumn get draft => boolean()(); + TextColumn get head => text().map(const MinimalPullRequestHeadConverter())(); + @JsonKey('html_url') + TextColumn get htmlUrl => text()(); + IntColumn get id => integer().autoIncrement()(); + @JsonKey('mergeable_state') + TextColumn get mergeableState => text().nullable()(); + IntColumn get number => integer()(); + TextColumn get repoSlug => text().nullable()(); + TextColumn get title => text()(); + @JsonKey('updated_at') + DateTimeColumn get updatedAt => dateTime().nullable()(); + TextColumn get user => text().map(const MinimalUserConverter())(); } final class MinimalPullRequestHead { final String ref; final String sha; + final MinimalRepositoryData repo; const MinimalPullRequestHead({ required this.ref, required this.sha, + required this.repo, }); - const MinimalPullRequestHead.empty() - : ref = '', - sha = ''; - - factory MinimalPullRequestHead.fromJson(Map json) { + factory MinimalPullRequestHead.fromJson(Map map) { return MinimalPullRequestHead( - ref: json['ref'] as String, - sha: json['sha'] as String, + ref: map['ref'] as String, + sha: map['sha'] as String, + repo: MinimalRepositoryData.fromJson(map['repo'] as Map), ); } - Map toJson() { - return { - 'ref': ref, - 'sha': sha, - }; - } + Map toJson() => { + 'ref': ref, + 'sha': sha, + 'repo': repo.toJson(), + }; } -@Entity() -final class MinimalRepository { - @Id(assignable: true) - int id; - String name; - String fullName; - String owner; - String? description; - - final ToMany pullRequests = ToMany(); +final class MinimalPullRequestHeadConverter + extends TypeConverter + with + JsonTypeConverter2> { + const MinimalPullRequestHeadConverter(); - MinimalRepository() - : id = 0, - name = '', - fullName = '', - owner = '', - description = ''; + @override + MinimalPullRequestHead fromSql(String fromDb) { + return fromJson(jsonDecode(fromDb)); + } - factory MinimalRepository.fromJson(Map json) => - MinimalRepository._( - id: json['id'] as int, - name: json['name'] as String, - fullName: json['full_name'] as String, - owner: json['owner']['login'] as String, - description: json['description'] as String?, - ); + @override + String toSql(MinimalPullRequestHead value) { + return jsonEncode(toJson(value)); + } - MinimalRepository._({ - required this.id, - required this.name, - required this.fullName, - required this.owner, - required this.description, - }); + @override + MinimalPullRequestHead fromJson(Map json) { + return MinimalPullRequestHead.fromJson(json); + } - RepositorySlug slug() => RepositorySlug(owner, name); + @override + Map toJson(MinimalPullRequestHead value) { + return value.toJson(); + } } -@Entity() -final class MinimalUser { - @Id(assignable: true) - int id; - String login; - String avatarUrl; - - MinimalUser() - : id = 0, - login = '', - avatarUrl = ''; - - factory MinimalUser.fromJson(Map json) { - return MinimalUser._( - id: json['id'] as int, - login: json['login'] as String, - avatarUrl: json['avatar_url'] as String, - ); - } +class MinimalRepository extends Table { + TextColumn get description => text().nullable()(); + @JsonKey('full_name') + TextColumn get fullName => text()(); + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + TextColumn get owner => text().map(const MinimalUserConverter())(); +} - MinimalUser._({ - required this.id, - required this.login, - required this.avatarUrl, - }); +class MinimalUser extends Table { + @JsonKey('avatar_url') + TextColumn get avatarUrl => text()(); + IntColumn get id => integer().autoIncrement()(); + TextColumn get login => text()(); } -/// [CombinedRepositoryStatus] -@Entity() -final class MinimalCombinedRepositoryStatus { - @Id() - int id = 0; - String? state; - String? sha; - int? totalCount; - @Transient() - List? statuses; +final class MinimalUserConverter extends TypeConverter + with JsonTypeConverter2> { + const MinimalUserConverter(); - final ToOne repository; + @override + MinimalUserData fromSql(String fromDb) { + return fromJson(jsonDecode(fromDb)); + } - MinimalCombinedRepositoryStatus() : repository = ToOne(); + @override + String toSql(MinimalUserData value) { + return jsonEncode(toJson(value)); + } - MinimalCombinedRepositoryStatus._({ - required this.id, - required this.state, - required this.sha, - required this.totalCount, - required this.statuses, - required this.repository, - }); + @override + MinimalUserData fromJson(Map json) { + return MinimalUserData.fromJson(json); + } - factory MinimalCombinedRepositoryStatus.fromJson(Map json) { - return MinimalCombinedRepositoryStatus._( - id: 0, - state: json['state'] as String?, - sha: json['sha'] as String?, - totalCount: json['total_count'] as int?, - statuses: (json['statuses'] as List) - .map((e) => RepositoryStatus.fromJson(e)) - .toList(growable: false), - repository: ToOne(target: MinimalRepository.fromJson(json['repository'])), - ); + @override + Map toJson(MinimalUserData value) { + return value.toJson(); } +} - String get statusesStored => - jsonEncode(statuses?.map((e) => e.toJson()).toList(growable: false)); - set statusesStored(String value) => - statuses = (jsonDecode(value) as List) - .map((e) => RepositoryStatus.fromJson(e)) - .toList(growable: false); +extension MinimalRepositoryExtension on MinimalRepositoryData { + RepositorySlug slug() => RepositorySlug(owner.login, name); } diff --git a/lib/models/json_enum.dart b/lib/models/json_enum.dart new file mode 100644 index 0000000..77ffc0c --- /dev/null +++ b/lib/models/json_enum.dart @@ -0,0 +1,33 @@ +import 'package:drift/drift.dart'; + +abstract class JsonEnum { + String get string; +} + +final class JsonEnumConverter> + extends TypeConverter + with JsonTypeConverter2 { + final List values; + + const JsonEnumConverter(this.values); + + @override + T fromJson(String json) { + return fromSql(json); + } + + @override + T fromSql(String fromDb) { + return values.firstWhere((element) => element.string == fromDb); + } + + @override + String toJson(T value) { + return toSql(value); + } + + @override + String toSql(T value) { + return value.string; + } +} diff --git a/lib/models/pull_request_info.dart b/lib/models/pull_request_info.dart index 9e19b8c..a5d986a 100644 --- a/lib/models/pull_request_info.dart +++ b/lib/models/pull_request_info.dart @@ -1,9 +1,9 @@ -import 'package:tokhub/models/github.dart'; +import 'package:tokhub/db/database.dart'; final class PullRequestInfo { /// The repository to which the pull request belongs. - final MinimalRepository repo; - final MinimalPullRequest pr; + final MinimalRepositoryData repo; + final MinimalPullRequestData pr; /// The assigned pull request issue number. final int number; @@ -46,14 +46,15 @@ final class PullRequestInfo { required this.commitSha, }); - factory PullRequestInfo.from(MinimalRepository repo, MinimalPullRequest pr) { + factory PullRequestInfo.from( + MinimalRepositoryData repo, MinimalPullRequestData pr) { return PullRequestInfo( repo: repo, pr: pr, number: pr.number, url: Uri.parse(pr.htmlUrl), - user: pr.user.target!.login, - avatar: pr.user.target!.avatarUrl, + user: pr.user.login, + avatar: pr.user.avatarUrl, branch: pr.head.ref, title: pr.title, state: PullRequestMergeableState.values.firstWhere( diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json deleted file mode 100644 index fd2b55c..0000000 --- a/lib/objectbox-model.json +++ /dev/null @@ -1,333 +0,0 @@ -{ - "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", - "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", - "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", - "entities": [ - { - "id": "1:4418640459449943997", - "lastPropertyId": "17:7352012974240986707", - "name": "MinimalCheckRun", - "properties": [ - { - "id": "1:5967602549453904706", - "name": "id", - "type": 6, - "flags": 129 - }, - { - "id": "10:4766122260221774679", - "name": "name", - "type": 9 - }, - { - "id": "11:7527966746125404340", - "name": "headSha", - "type": 9, - "flags": 2048, - "indexId": "70:4988381078771371172" - }, - { - "id": "12:1094076447792246980", - "name": "detailsUrl", - "type": 9 - }, - { - "id": "13:5553707307975891820", - "name": "startedAt", - "type": 10 - }, - { - "id": "14:2164188484820672395", - "name": "completedAt", - "type": 10 - }, - { - "id": "17:7352012974240986707", - "name": "conclusionStored", - "type": 9 - } - ], - "relations": [] - }, - { - "id": "2:1840556005267755862", - "lastPropertyId": "12:8201836691167978625", - "name": "MinimalPullRequest", - "properties": [ - { - "id": "1:2250080409803715816", - "name": "id", - "type": 6, - "flags": 129 - }, - { - "id": "2:1329234623288186818", - "name": "userId", - "type": 11, - "flags": 520, - "indexId": "1:4455167633753291303", - "relationTarget": "MinimalUser" - }, - { - "id": "3:9133370166830192543", - "name": "repoId", - "type": 11, - "flags": 520, - "indexId": "2:3939024267872595159", - "relationTarget": "MinimalRepository" - }, - { - "id": "4:2821326731407505290", - "name": "number", - "type": 6 - }, - { - "id": "5:2407858717646121618", - "name": "draft", - "type": 1 - }, - { - "id": "6:3858855405142987423", - "name": "title", - "type": 9 - }, - { - "id": "7:4898903109888383039", - "name": "mergeableState", - "type": 9 - }, - { - "id": "8:6399682120980620342", - "name": "htmlUrl", - "type": 9 - }, - { - "id": "11:49832800322974285", - "name": "headStored", - "type": 9 - }, - { - "id": "12:8201836691167978625", - "name": "updatedAt", - "type": 10 - } - ], - "relations": [] - }, - { - "id": "3:7454220200024985252", - "lastPropertyId": "10:3134731416564791237", - "name": "MinimalRepository", - "properties": [ - { - "id": "1:1339865120969947600", - "name": "id", - "type": 6, - "flags": 129 - }, - { - "id": "7:2877598964875068475", - "name": "name", - "type": 9 - }, - { - "id": "8:7964607348819070988", - "name": "fullName", - "type": 9 - }, - { - "id": "9:335766667478709394", - "name": "owner", - "type": 9 - }, - { - "id": "10:3134731416564791237", - "name": "description", - "type": 9 - } - ], - "relations": [ - { - "id": "1:8817405110228780130", - "name": "pullRequests", - "targetId": "2:1840556005267755862" - } - ] - }, - { - "id": "5:3727371350322499011", - "lastPropertyId": "3:6798998558940488246", - "name": "MinimalUser", - "properties": [ - { - "id": "1:1072855727434484395", - "name": "id", - "type": 6, - "flags": 129 - }, - { - "id": "2:3761359219117039511", - "name": "login", - "type": 9 - }, - { - "id": "3:6798998558940488246", - "name": "avatarUrl", - "type": 9 - } - ], - "relations": [] - }, - { - "id": "6:1442867974319267869", - "lastPropertyId": "7:162516385934130147", - "name": "MinimalCombinedRepositoryStatus", - "properties": [ - { - "id": "1:309919272432209285", - "name": "id", - "type": 6, - "flags": 1 - }, - { - "id": "2:92423723094406461", - "name": "state", - "type": 9 - }, - { - "id": "3:5506862424672175663", - "name": "sha", - "type": 9 - }, - { - "id": "4:4517039254136899490", - "name": "totalCount", - "type": 6 - }, - { - "id": "5:5012490555459450834", - "name": "repositoryId", - "type": 11, - "flags": 520, - "indexId": "69:3802515854405888816", - "relationTarget": "MinimalRepository" - }, - { - "id": "7:162516385934130147", - "name": "statusesStored", - "type": 9 - } - ], - "relations": [] - } - ], - "lastEntityId": "6:1442867974319267869", - "lastIndexId": "70:4988381078771371172", - "lastRelationId": "1:8817405110228780130", - "lastSequenceId": "0:0", - "modelVersion": 5, - "modelVersionParserMinimum": 5, - "retiredEntityUids": [ - 3293700315209556209 - ], - "retiredIndexUids": [ - 7169265391844706465, - 3432698025723709643, - 4799294847817696519, - 1562757974539166005, - 7951425751015502999, - 5473635256570218811, - 5818078850381377781, - 4517469426685438475, - 7340473677221596541, - 392347806570523083, - 6343979492137567483, - 867383908061758984, - 4593078447465634813, - 5848539532662496786, - 8451389724140177316, - 1250415049235964419, - 6272385253121129003, - 3672553668214036560, - 9108772782898100375, - 2171033427612207729, - 8833871366352366437, - 8724970025249515414, - 5104149235731375291, - 2451409875153169908, - 6305891926002951241, - 102027832767702113, - 5270415993850231151, - 8070640301183692015, - 7445492682157735074, - 2723873247476874219, - 5944672784301724296, - 3801273348087540917, - 1185028626068347262, - 7719261541571034657, - 8135674824588940928, - 9105615919773709762, - 6230503503506801670, - 3263617258500231963, - 2712110399389280175, - 4339560101453294843, - 5083498020484808895, - 7242556340756323278, - 4021702818691795415, - 1107999432213466573, - 1738232914976049348, - 7892679825803935993, - 5492224135537079047, - 7733072702230316708, - 7920962969692918926, - 9144224576334852059, - 8726385298578647269, - 1850134290330076634, - 7557882029415757891, - 3308785944314071944, - 775947301812401354, - 7704821179495470507, - 6480170536625885559, - 6097836402214877765, - 1769816465003614231, - 6100198783661298395, - 7534286666429018128, - 1130703143526037629, - 1601648159074992018, - 8636426807208800214, - 759899371305303668 - ], - "retiredPropertyUids": [ - 2443815012222847181, - 6275208469376351912, - 8022243478778949827, - 8193091727694841393, - 9186106093913422156, - 7487701915536357917, - 3625906047562206591, - 7802791187581914288, - 1120242529243141498, - 3908758175661816567, - 7353920831090378835, - 4000513433170707729, - 6930834336306067288, - 124838008405705728, - 6947221470225447562, - 8890382764566844105, - 7005133943000908899, - 7205709040522936588, - 7034359975351627369, - 3247508726372721942, - 6020991578874622374, - 7649475187474284563, - 6925941947529804375, - 2678217806347797383, - 4983422314485991674, - 7076785833471662397, - 6559538485455455745, - 1924987805376985150, - 8428210103948497866 - ], - "retiredRelationUids": [], - "version": 1 -} \ No newline at end of file diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 7309088..02a92f9 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,92 +3,96 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tokhub/logger.dart'; import 'package:tokhub/models/github.dart'; import 'package:tokhub/pages/settings_page.dart'; -import 'package:tokhub/providers/check_runs.dart'; -import 'package:tokhub/providers/combined_repository_status.dart'; +import 'package:tokhub/providers/database.dart'; import 'package:tokhub/providers/github.dart'; -import 'package:tokhub/providers/objectbox.dart'; -import 'package:tokhub/providers/pull_request.dart'; -import 'package:tokhub/providers/repository.dart'; +import 'package:tokhub/providers/repos/pulls.dart'; +import 'package:tokhub/providers/orgs/repos.dart'; import 'package:tokhub/providers/settings.dart'; import 'package:tokhub/views.dart'; +import 'package:tokhub/widgets/home_view.dart'; import 'package:tokhub/widgets/debug_log_view.dart'; import 'package:tokhub/widgets/pull_requests_view.dart'; import 'package:tokhub/widgets/repositories_view.dart'; class HomePage extends ConsumerWidget { + static const org = 'TokTok'; + const HomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return ref.watch(settingsProvider).when( - loading: () => const CircularProgressIndicator(), - error: (error, _) => Text('Error: $error'), - data: (settings) => Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(settings.mainView.title), - ), - drawer: Drawer( - child: ListView( - padding: EdgeInsets.zero, + loading: () => const CircularProgressIndicator(), + error: (error, _) => Text('Error: $error'), + data: (settings) => _build(context, ref, settings)); + } + + Widget _build(BuildContext context, WidgetRef ref, SettingsState settings) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(settings.mainView.title), + ), + drawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.inversePrimary, + image: const DecorationImage( + image: AssetImage('assets/images/tokhub.png'), + fit: BoxFit.none, + alignment: Alignment.centerRight, + scale: 7, + opacity: 0.7, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.inversePrimary, - image: const DecorationImage( - image: AssetImage('assets/images/tokhub.png'), - fit: BoxFit.none, - alignment: Alignment.centerRight, - scale: 7, - opacity: 0.7, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'TokHub', - style: Theme.of(context).textTheme.headlineSmall, - ), - ref.watch(currentUserProvider).when( - loading: () => const CircularProgressIndicator(), - error: (error, _) => Text('Error: $error'), - data: (user) { - final style = - Theme.of(context).textTheme.bodyMedium; - return user != null - ? Text('Authenticated: ${user.login}', - style: style) - : Text('Not authenticated', style: style); - }, - ), - ], - ), + Text( + 'TokHub', + style: Theme.of(context).textTheme.headlineSmall, ), - ..._buildDrawerItems(context, ref, settings), + ref.watch(currentUserProvider).when( + loading: () => const CircularProgressIndicator(), + error: (error, _) => Text('Error: $error'), + data: (user) { + final style = Theme.of(context).textTheme.bodyMedium; + return user != null + ? Text('Authenticated: ${user.login}', + style: style) + : Text('Not authenticated', style: style); + }, + ), ], ), ), - body: _buildBody(settings.mainView), - floatingActionButton: FloatingActionButton( - onPressed: () async { - debugPrint('Refreshing'); - final store = await ref.watch(objectBoxProvider.future); - store.box().removeAll(); - store.box().removeAll(); - store.box().removeAll(); - store.box().removeAll(); - ref.invalidate(checkRunsProvider); - ref.invalidate(combinedRepositoryStatusProvider); - ref.invalidate(combinedRepositoryStatusProvider); - ref.invalidate(pullRequestsProvider); - ref.invalidate(repositoriesProvider); - }, - tooltip: 'Refresh', - child: const Icon(Icons.refresh), - ), // This trailing comma makes auto-formatting nicer for build methods. - ), - ); + ..._buildDrawerItems(context, ref, settings), + ], + ), + ), + body: _buildBody(settings.mainView), + floatingActionButton: FloatingActionButton( + onPressed: () async { + debugPrint('Refreshing'); + final db = await ref.read(databaseProvider.future); + await db.clear(); + await repositoriesRefresh(ref, org); + for (final repo + in await ref.watch(repositoriesProvider(org).future)) { + await pullRequestsRefresh(ref, repo.slug()); + for (final pr + in await ref.watch(pullRequestsProvider(repo.slug()).future)) { + await pullRequestRefresh(ref, repo.slug(), pr.number); + } + } + }, + tooltip: 'Refresh', + child: const Icon(Icons.refresh), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); } List _buildDrawerItems( @@ -126,13 +130,11 @@ class HomePage extends ConsumerWidget { Widget _buildBody(MainView view) { switch (view) { case MainView.home: - return const Center( - child: Text('Home'), - ); + return const HomeView(); case MainView.repos: - return const RepositoriesView(org: 'TokTok'); + return const RepositoriesView(org: org); case MainView.pullRequests: - return const PullRequestsView(org: 'TokTok'); + return const PullRequestsView(org: org); case MainView.settings: return const SettingsPage(); case MainView.logs: diff --git a/lib/providers/check_runs.dart b/lib/providers/check_runs.dart deleted file mode 100644 index d7c1f4a..0000000 --- a/lib/providers/check_runs.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:github/github.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:tokhub/logger.dart'; -import 'package:tokhub/models/github.dart'; -import 'package:tokhub/objectbox.g.dart'; -import 'package:tokhub/providers/github.dart'; -import 'package:tokhub/providers/objectbox.dart'; - -part 'check_runs.g.dart'; - -const _logger = Logger(['CheckRunsProvider']); - -const _previewHeader = 'application/vnd.github.antiope-preview+json'; - -@riverpod -Future> checkRuns( - Ref ref, - MinimalRepository repo, - MinimalPullRequest pullRequest, { - required bool force, -}) async { - final slug = repo.slug(); - final commitSha = pullRequest.head.sha; - - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - final fetched = - box.query(MinimalCheckRun_.headSha.equals(commitSha)).build().find(); - if (fetched.isNotEmpty) { - _logger.v('Found ${fetched.length} stored check runs for $slug@$commitSha'); - return fetched; - } - - if (!force && - pullRequest.updatedAt! - .isBefore(DateTime.now().subtract(maxPullRequestAge))) { - _logger.v('Pull request ${slug.name}/${pullRequest.head.ref} is too old' - ', not fetching check runs'); - return const []; - } - - final ids = - await ref.watch(_fetchAndStoreCheckRunsProvider(repo, commitSha).future); - return box.getMany(ids).whereType().toList(growable: false); -} - -Future checkRunsRefresh( - WidgetRef ref, MinimalRepository repo, String commitSha) async { - ref.invalidate(_fetchAndStoreCheckRunsProvider(repo, commitSha)); - await ref.watch(_fetchAndStoreCheckRunsProvider(repo, commitSha).future); -} - -@riverpod -Future> _fetchAndStoreCheckRuns( - Ref ref, MinimalRepository repo, String commitSha) async { - final slug = repo.slug(); - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - - // Fetch new check runs. - final runs = - await ref.watch(_githubCheckRunsProvider(slug, commitSha).future); - - // Delete existing check runs for the same commit. - box.query(MinimalCheckRun_.headSha.equals(commitSha)).build().remove(); - - // Store the new check runs. - final ids = box.putMany(runs); - _logger.v('Stored check runs: ${runs.length}'); - - return ids; -} - -@riverpod -Future> _githubCheckRuns( - Ref ref, RepositorySlug slug, String commitSha) async { - final client = await ref.watch(githubClientProvider.future); - if (client == null) { - return const []; - } - - _logger.d('Fetching check runs for $slug@$commitSha'); - return PaginationHelper(client) - .objects, MinimalCheckRun>( - 'GET', - 'repos/$slug/commits/$commitSha/check-runs', - MinimalCheckRun.fromJson, - statusCode: StatusCodes.OK, - preview: _previewHeader, - arrayKey: 'check_runs', - ) - .toList(); -} diff --git a/lib/providers/check_status.dart b/lib/providers/check_status.dart index 8851aa4..4fedfe0 100644 --- a/lib/providers/check_status.dart +++ b/lib/providers/check_status.dart @@ -1,11 +1,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:github/github.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:tokhub/logger.dart'; import 'package:tokhub/models/check_status.dart'; -import 'package:tokhub/models/github.dart'; -import 'package:tokhub/providers/check_runs.dart'; -import 'package:tokhub/providers/combined_repository_status.dart'; -import 'package:tokhub/providers/pull_request.dart'; +import 'package:tokhub/providers/repos/commits/check_runs.dart'; +import 'package:tokhub/providers/repos/commits/status.dart'; +import 'package:tokhub/providers/repos/pulls.dart'; part 'check_status.g.dart'; @@ -13,60 +13,52 @@ const _logger = Logger(['CheckStatusProvider']); @riverpod Future> checkStatus( - Ref ref, - MinimalRepository repo, - MinimalPullRequest pullRequest, { - required bool force, -}) async { - final statuses = (await ref.watch(combinedRepositoryStatusProvider( - repo, - pullRequest, - force: force, - ).future)) - .statuses; + Ref ref, RepositorySlug slug, String headSha) async { + final statuses = await ref.watch(combinedRepositoryStatusProvider( + slug, + headSha, + ).future); final runs = await ref.watch(checkRunsProvider( - repo, - pullRequest, - force: force, + slug, + headSha, ).future); final byName = {}; for (final run in runs) { byName[run.name] = CheckStatus( name: run.name, - conclusion: run.conclusion, + conclusion: run.conclusion ?? CheckStatusConclusion.empty, description: _completedDescription( - run.conclusion, + run.conclusion ?? CheckStatusConclusion.empty, run.startedAt, run.completedAt, ), detailsUrl: Uri.parse(run.detailsUrl), ); } - if (statuses != null) { - for (final status in statuses) { - byName[status.context!] = CheckStatus( - name: status.context!, - conclusion: _statusConclusion(status.state), - description: '${_capitalize(status.state)} — ${status.description}', - detailsUrl: - status.targetUrl == null ? null : Uri.parse(status.targetUrl!), - ); - } + for (final status in statuses) { + byName[status.context] = CheckStatus( + name: status.context, + conclusion: _statusConclusion(status.state), + description: status.description == null + ? _capitalize(status.state) + : '${_capitalize(status.state)} — ${status.description}', + detailsUrl: + status.targetUrl == null ? null : Uri.parse(status.targetUrl!), + ); } return byName.values.toList(growable: false); } -Future checkStatusRefresh(WidgetRef ref, MinimalRepository repo, - MinimalPullRequest pullRequest) async { - await pullRequestRefresh(ref, repo, pullRequest); - final newPr = - await ref.watch(pullRequestProvider(repo, pullRequest.number).future); +Future checkStatusRefresh( + WidgetRef ref, RepositorySlug slug, String headSha, int pullRequest) async { + await pullRequestRefresh(ref, slug, pullRequest); + final newPr = await ref.watch(pullRequestProvider(slug, pullRequest).future); _logger.v( - 'Refreshing check status for ${repo.slug()}#${newPr.number} (${newPr.head.sha})'); - await combinedRepositoryStatusRefresh(ref, repo, newPr.head.sha); - await checkRunsRefresh(ref, repo, newPr.head.sha); + 'Refreshing check status for $slug#${newPr.number} (${newPr.head.sha})'); + await combinedRepositoryStatusRefresh(ref, slug, newPr.head.sha); + await checkRunsRefresh(ref, slug, newPr.head.sha); } String _capitalize(String? string) { diff --git a/lib/providers/combined_repository_status.dart b/lib/providers/combined_repository_status.dart deleted file mode 100644 index 62dceb6..0000000 --- a/lib/providers/combined_repository_status.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:github/github.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:tokhub/logger.dart'; -import 'package:tokhub/models/github.dart'; -import 'package:tokhub/objectbox.g.dart'; -import 'package:tokhub/providers/github.dart'; -import 'package:tokhub/providers/objectbox.dart'; - -part 'combined_repository_status.g.dart'; - -const _logger = Logger(['CombinedRepositoryStatusProvider']); - -@riverpod -Future combinedRepositoryStatus( - Ref ref, - MinimalRepository repo, - MinimalPullRequest pullRequest, { - required bool force, -}) async { - final slug = repo.slug(); - final commitSha = pullRequest.head.sha; - - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - final fetched = box - .query(MinimalCombinedRepositoryStatus_.repository.equals(repo.id) & - MinimalCombinedRepositoryStatus_.sha.equals(commitSha)) - .build() - .find(); - if (fetched.isNotEmpty) { - _logger.v('Found stored combined repository status for $slug@$commitSha ' - '(${fetched.first.statuses?.length} statuses)'); - return fetched.first; - } - - if (!force && - pullRequest.updatedAt! - .isBefore(DateTime.now().subtract(maxPullRequestAge))) { - _logger.v('Pull request ${repo.name}/${pullRequest.head.ref} is too old' - ', not fetching combined statuses'); - return MinimalCombinedRepositoryStatus(); - } - - final id = await ref.watch( - _fetchAndStoreCombinedRepositoryStatusProvider(repo, commitSha).future); - return box.get(id)!; -} - -Future combinedRepositoryStatusRefresh( - WidgetRef ref, MinimalRepository repo, String commitSha) async { - ref.invalidate( - _fetchAndStoreCombinedRepositoryStatusProvider(repo, commitSha)); - await ref.watch( - _fetchAndStoreCombinedRepositoryStatusProvider(repo, commitSha).future); -} - -@riverpod -Future _fetchAndStoreCombinedRepositoryStatus( - Ref ref, MinimalRepository repo, String commitSha) async { - final slug = repo.slug(); - - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - - // Fetch the combined status from GitHub. - final status = await ref - .watch(_githubCombinedRepositoryStatusProvider(slug, commitSha).future); - - // Delete existing combined status for the same commit. - box - .query(MinimalCombinedRepositoryStatus_.repository.equals(repo.id) & - MinimalCombinedRepositoryStatus_.sha.equals(commitSha)) - .build() - .remove(); - - // Store the new combined status. - final id = box.put(status); - _logger.v('Stored combined repository status: $slug@$commitSha ' - '(${status.statuses?.length} statuses)'); - - return id; -} - -@riverpod -Future _githubCombinedRepositoryStatus( - Ref ref, RepositorySlug slug, String commitSha) async { - final client = await ref.watch(githubClientProvider.future); - if (client == null) { - throw StateError('No client available'); - } - - _logger.d('Fetching commit status for $slug@$commitSha'); - return client.getJSON, MinimalCombinedRepositoryStatus>( - '/repos/$slug/commits/$commitSha/status', - convert: MinimalCombinedRepositoryStatus.fromJson, - statusCode: StatusCodes.OK, - ); -} diff --git a/lib/providers/database.dart b/lib/providers/database.dart new file mode 100644 index 0000000..392fc8c --- /dev/null +++ b/lib/providers/database.dart @@ -0,0 +1,13 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tokhub/db/database.dart'; +import 'package:tokhub/db/shared.dart'; + +part 'database.g.dart'; + +@riverpod +Future database(Ref ref) async { + final db = constructDb(); + ref.onDispose(() => db.then((value) => value.close())); + return db; +} diff --git a/lib/providers/objectbox.dart b/lib/providers/objectbox.dart deleted file mode 100644 index 40ee967..0000000 --- a/lib/providers/objectbox.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:tokhub/logger.dart'; - -import '../objectbox.g.dart'; -part 'objectbox.g.dart'; - -const _logger = Logger(['ObjectBox']); - -@riverpod -Future objectBox(Ref ref) async { - final directory = await getApplicationDocumentsDirectory(); - final path = p.join(directory.path, 'TokHub', 'objectbox'); - final dir = await Directory(path).create(recursive: true); - _logger.i('ObjectBox directory: ${dir.path}'); - final store = await openStore( - directory: path, - macosApplicationGroup: '83SSMZ4T7X.tokhub', - ); - ref.onDispose(() => store.close()); - return store; -} diff --git a/lib/providers/orgs/repos.dart b/lib/providers/orgs/repos.dart new file mode 100644 index 0000000..06a35fa --- /dev/null +++ b/lib/providers/orgs/repos.dart @@ -0,0 +1,56 @@ +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:github/github.dart' show PaginationHelper; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tokhub/db/database.dart'; +import 'package:tokhub/logger.dart'; +import 'package:tokhub/providers/database.dart'; +import 'package:tokhub/providers/github.dart'; + +part 'repos.g.dart'; + +const _logger = Logger(['RepositoryProvider']); + +@riverpod +Future> repositories(Ref ref, String org) async { + final db = await ref.watch(databaseProvider.future); + final stored = await (db.select(db.minimalRepository) + ..orderBy([(r) => OrderingTerm(expression: r.fullName)])) + .get(); + return stored; +} + +Future repositoriesRefresh(WidgetRef ref, String org) async { + ref.invalidate(_fetchAndStoreRepositoriesProvider(org)); + await ref.watch(_fetchAndStoreRepositoriesProvider(org).future); +} + +@riverpod +Future _fetchAndStoreRepositories(Ref ref, String org) async { + final repos = await ref.watch(_githubRepositoriesProvider(org).future); + final db = await ref.watch(databaseProvider.future); + for (final repo in repos) { + await db.into(db.minimalRepository).insertOnConflictUpdate(repo); + } + _logger.v('Stored ${repos.length} repositories'); +} + +@riverpod +Future> _githubRepositories( + Ref ref, String org) async { + final client = await ref.watch(githubClientProvider.future); + if (client == null) { + return const []; + } + + _logger.d('Fetching repositories for $org'); + final repos = await PaginationHelper(client) + .objects, MinimalRepositoryData>( + 'GET', + '/orgs/$org/repos', + _logger.catching(MinimalRepositoryData.fromJson), + ) + .toList(); + _logger.d('Fetched ${repos.length} repositories for $org'); + return repos; +} diff --git a/lib/providers/pull_request.dart b/lib/providers/pull_request.dart deleted file mode 100644 index c3b9d8d..0000000 --- a/lib/providers/pull_request.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:github/github.dart' show StatusCodes; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:tokhub/logger.dart'; -import 'package:tokhub/models/github.dart'; -import 'package:tokhub/objectbox.g.dart'; -import 'package:tokhub/providers/github.dart'; -import 'package:tokhub/providers/objectbox.dart'; -import 'package:tokhub/providers/repository.dart'; - -part 'pull_request.g.dart'; - -const _logger = Logger(['PullRequestProvider']); - -@riverpod -Future pullRequest( - Ref ref, MinimalRepository repo, int number) async { - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - final fetched = box - .query(MinimalPullRequest_.repo.equals(repo.id)) - .build() - .find() - .where((pr) => pr.number == number && pr.mergeableState != '') - .toList(growable: false); - if (fetched.isNotEmpty) { - _logger.v('Found stored pull request for ${repo.slug()}#$number ' - '(${fetched.first.mergeableState})'); - return fetched.first; - } - return box.get( - await ref.watch(_fetchAndStorePullRequestProvider(repo, number).future))!; -} - -Future pullRequestRefresh( - WidgetRef ref, - MinimalRepository repo, - MinimalPullRequest pullRequest, -) async { - ref.invalidate(_fetchAndStorePullRequestProvider(repo, pullRequest.number)); - await ref.watch( - _fetchAndStorePullRequestProvider(repo, pullRequest.number).future); - ref.invalidate(pullRequestProvider(repo, pullRequest.number)); -} - -@riverpod -Future> pullRequests( - Ref ref, MinimalRepository repo) async { - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - final fetched = - box.query(MinimalPullRequest_.repo.equals(repo.id)).build().find(); - if (fetched.isNotEmpty) { - _logger.v('Found ${fetched.length} ' - 'stored pull requests for ${repo.slug()}'); - return fetched; - } - - final ids = await ref.watch(_fetchAndStorePullRequestsProvider(repo).future); - return box - .getMany(ids) - .whereType() - .toList(growable: false); -} - -Future pullRequestsRefresh(WidgetRef ref, MinimalRepository repo) async { - ref.invalidate(_fetchAndStorePullRequestsProvider(repo)); - await ref.watch(_fetchAndStorePullRequestsProvider(repo).future); - ref.invalidate(pullRequestsProvider(repo)); - ref.invalidate(repositoriesProvider(org: repo.owner)); -} - -@riverpod -Future _fetchAndStorePullRequest( - Ref ref, MinimalRepository repo, int number) async { - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - - final pr = await ref.watch(_githubPullRequestProvider(repo, number).future); - final id = box.put(pr..repo.target = repo); - _logger.d('Stored pull request: ${repo.slug()}#$number (${pr.head.sha})'); - - return id; -} - -@riverpod -Future> _fetchAndStorePullRequests( - Ref ref, MinimalRepository repo) async { - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - - // Fetch new pull requests. - final prs = await ref.watch(_githubPullRequestsProvider(repo).future); - - // Delete old pull requests. - box.query(MinimalPullRequest_.repo.equals(repo.id)).build().remove(); - - final repoBox = store.box(); - final userBox = store.box(); - for (final pr in prs) { - if (repoBox.get(pr.repo.target!.id) == null) { - repoBox.put(pr.repo.target!); - } else { - pr.repo.attach(store); - } - if (userBox.get(pr.user.target!.id) == null) { - userBox.put(pr.user.target!); - } else { - pr.user.attach(store); - } - } - - // Store new pull requests. - final ids = box.putMany(prs); - - for (final pr in prs) { - if (pr.repo.targetId != pr.repo.target!.id) { - _logger.e('Pull request ID ${pr.repo.target!.id} does not match ' - 'target ID ${pr.repo.targetId}'); - continue; - } - if (pr.repo.target!.id != repo.id) { - _logger.e('Pull request ${pr.repo.target!.slug()}#${pr.number} ' - 'does not belong to ${repo.slug()}'); - continue; - } - pr.repo.target!.pullRequests.add(pr); - repoBox.put(pr.repo.target!); - } - - _logger.d('Stored pull requests: ${ids.length} for ${repo.slug()}'); - - return ids; -} - -@riverpod -Future _githubPullRequest( - Ref ref, MinimalRepository repo, int number) async { - final slug = repo.slug(); - final client = await ref.watch(githubClientProvider.future); - if (client == null) { - throw StateError('No client available'); - } - - _logger.d('Fetching pull request $slug#$number'); - return client.getJSON( - '/repos/$slug/pulls/$number', - convert: MinimalPullRequest.fromJson, - statusCode: StatusCodes.OK, - ); -} - -@riverpod -Future> _githubPullRequests( - Ref ref, MinimalRepository repo) async { - final slug = repo.slug(); - final client = await ref.watch(githubClientProvider.future); - if (client == null) { - return const []; - } - - _logger.d('Fetching pull requests for $slug'); - return client.pullRequests - .list(slug) - .map((pr) => - MinimalPullRequest.fromJson(jsonDecode(jsonEncode(pr.toJson())))) - .toList(); -} diff --git a/lib/providers/pull_request_info.dart b/lib/providers/pull_request_info.dart deleted file mode 100644 index 0fe9a1c..0000000 --- a/lib/providers/pull_request_info.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:tokhub/models/github.dart'; -import 'package:tokhub/models/pull_request_info.dart'; -import 'package:tokhub/providers/pull_request.dart'; -import 'package:tokhub/providers/repository.dart'; - -part 'pull_request_info.g.dart'; - -@riverpod -Future> pullRequestInfo( - Ref ref, - MinimalRepository repo, -) async { - final prs = (await ref.watch(pullRequestsProvider(repo).future)) - .map((pr) => PullRequestInfo.from(repo, pr)) - .toList(growable: false); - prs.sort((a, b) => b.number.compareTo(a.number)); - return prs; -} - -@riverpod -Future)>> pullRequestInfos( - Ref ref, { - required String org, -}) async { - final repos = await ref.watch(repositoriesProvider(org: org).future); - repos.sort((a, b) => a.name.compareTo(b.name)); - return (await Future.wait(repos.map((repo) async => - (repo, await ref.watch(pullRequestInfoProvider(repo).future))))) - .where((element) => element.$2.isNotEmpty) - .toList(growable: false); -} diff --git a/lib/providers/repos/commits/check_runs.dart b/lib/providers/repos/commits/check_runs.dart new file mode 100644 index 0000000..6c0e5da --- /dev/null +++ b/lib/providers/repos/commits/check_runs.dart @@ -0,0 +1,72 @@ +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:github/github.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tokhub/db/database.dart'; +import 'package:tokhub/logger.dart'; +import 'package:tokhub/providers/database.dart'; +import 'package:tokhub/providers/github.dart'; + +part 'check_runs.g.dart'; + +const _logger = Logger(['CheckRunsProvider']); + +const _previewHeader = 'application/vnd.github.antiope-preview+json'; + +@riverpod +Future> checkRuns( + Ref ref, RepositorySlug slug, String headSha) async { + final db = await ref.watch(databaseProvider.future); + + final fetched = await (db.select(db.minimalCheckRun) + ..where((run) { + return run.repoSlug.equals(slug.fullName) & + run.headSha.equals(headSha); + })) + .get(); + + return fetched; +} + +Future checkRunsRefresh( + WidgetRef ref, RepositorySlug slug, String commitSha) async { + ref.invalidate(_fetchAndStoreCheckRunsProvider(slug, commitSha)); + await ref.watch(_fetchAndStoreCheckRunsProvider(slug, commitSha).future); +} + +@riverpod +Future _fetchAndStoreCheckRuns( + Ref ref, RepositorySlug slug, String commitSha) async { + // Fetch new check runs. + final runs = + await ref.watch(_githubCheckRunsProvider(slug, commitSha).future); + + // Store the new check runs. + final db = await ref.watch(databaseProvider.future); + + for (final run in runs) { + await db.into(db.minimalCheckRun).insertOnConflictUpdate(run); + } +} + +@riverpod +Future> _githubCheckRuns( + Ref ref, RepositorySlug slug, String commitSha) async { + final client = await ref.watch(githubClientProvider.future); + if (client == null) { + return const []; + } + + _logger.d('Fetching check runs for $slug@$commitSha'); + return PaginationHelper(client) + .objects, MinimalCheckRunData>( + 'GET', + 'repos/$slug/commits/$commitSha/check-runs', + _logger.catching(MinimalCheckRunData.fromJson), + statusCode: StatusCodes.OK, + preview: _previewHeader, + arrayKey: 'check_runs', + ) + .map((cr) => cr.copyWith(repoSlug: Value(slug.fullName))) + .toList(); +} diff --git a/lib/providers/repos/commits/status.dart b/lib/providers/repos/commits/status.dart new file mode 100644 index 0000000..a15c8ed --- /dev/null +++ b/lib/providers/repos/commits/status.dart @@ -0,0 +1,74 @@ +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:github/github.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tokhub/db/database.dart'; +import 'package:tokhub/logger.dart'; +import 'package:tokhub/providers/database.dart'; +import 'package:tokhub/providers/github.dart'; + +part 'status.g.dart'; + +const _logger = Logger(['CombinedRepositoryStatusProvider']); + +@riverpod +Future> combinedRepositoryStatus( + Ref ref, RepositorySlug slug, String headSha) async { + final db = await ref.watch(databaseProvider.future); + final fetched = await (db.select(db.minimalCommitStatus) + ..where((status) { + return status.repoSlug.equals(slug.fullName) & + status.headSha.equals(headSha); + })) + .get(); + _logger.v('Found stored combined repository status for $slug@$headSha ' + '(${fetched.length} statuses)'); + return fetched; +} + +Future combinedRepositoryStatusRefresh( + WidgetRef ref, RepositorySlug slug, String commitSha) async { + ref.invalidate( + _fetchAndStoreCombinedRepositoryStatusProvider(slug, commitSha)); + await ref.watch( + _fetchAndStoreCombinedRepositoryStatusProvider(slug, commitSha).future); +} + +@riverpod +Future _fetchAndStoreCombinedRepositoryStatus( + Ref ref, RepositorySlug slug, String commitSha) async { + // Fetch the combined status from GitHub. + final statuses = await ref + .watch(_githubCombinedRepositoryStatusProvider(slug, commitSha).future); + + final db = await ref.watch(databaseProvider.future); + + // Store the new statuses. + for (final status in statuses) { + await db.into(db.minimalCommitStatus).insertOnConflictUpdate(status); + } +} + +@riverpod +Future> _githubCombinedRepositoryStatus( + Ref ref, RepositorySlug slug, String commitSha) async { + final client = await ref.watch(githubClientProvider.future); + if (client == null) { + throw StateError('No client available'); + } + + _logger.d('Fetching commit status for $slug@$commitSha'); + final statuses = + client.getJSON, List>( + '/repos/$slug/commits/$commitSha/status', + convert: (crs) => (crs['statuses'] as List) + .map((json) => _logger + .catching(MinimalCommitStatusData.fromJson)(json) + .copyWith( + repoSlug: Value(slug.fullName), headSha: Value(commitSha))) + .toList(), + statusCode: StatusCodes.OK, + ); + + return statuses; +} diff --git a/lib/providers/repos/pulls.dart b/lib/providers/repos/pulls.dart new file mode 100644 index 0000000..e829f03 --- /dev/null +++ b/lib/providers/repos/pulls.dart @@ -0,0 +1,133 @@ +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:github/github.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tokhub/db/database.dart'; +import 'package:tokhub/logger.dart'; +import 'package:tokhub/providers/database.dart'; +import 'package:tokhub/providers/github.dart'; +import 'package:tokhub/providers/orgs/repos.dart'; + +part 'pulls.g.dart'; + +const _logger = Logger(['PullRequestProvider']); + +@riverpod +Future pullRequest( + Ref ref, RepositorySlug slug, int number) async { + final db = await ref.watch(databaseProvider.future); + final fetched = await (db.select(db.minimalPullRequest) + ..where((pr) { + return pr.repoSlug.equals(slug.fullName) & + pr.number.equals(number) & + pr.mergeableState.isNotNull(); + })) + .getSingle(); + _logger.v( + 'Found stored pull request for $slug#$number (${fetched.mergeableState})'); + return fetched; +} + +Future pullRequestRefresh( + WidgetRef ref, + RepositorySlug slug, + int number, +) async { + ref.invalidate(_fetchAndStorePullRequestProvider(slug, number)); + await ref.watch(_fetchAndStorePullRequestProvider(slug, number).future); + ref.invalidate(pullRequestProvider(slug, number)); +} + +@riverpod +Future> pullRequests( + Ref ref, RepositorySlug slug) async { + final db = await ref.watch(databaseProvider.future); + + _logger.v('Fetching pull requests for $slug'); + final fetched = (await (db.select(db.minimalPullRequest) + ..orderBy([(pr) => OrderingTerm.desc(pr.number)])) + .get()) + .where((pr) { + return pr.base.repo.fullName == slug.fullName; + }).toList(); + + _logger.v('Found ${fetched.length} stored pull requests for $slug'); + return fetched; +} + +Future pullRequestsRefresh(WidgetRef ref, RepositorySlug slug) async { + ref.invalidate(_fetchAndStorePullRequestsProvider(slug)); + await ref.watch(_fetchAndStorePullRequestsProvider(slug).future); + ref.invalidate(pullRequestsProvider(slug)); + ref.invalidate(repositoriesProvider(slug.owner)); +} + +@riverpod +Future _fetchAndStorePullRequest( + Ref ref, RepositorySlug slug, int number) async { + final db = await ref.watch(databaseProvider.future); + + final pr = await ref.watch(_githubPullRequestProvider(slug, number).future); + await db.into(db.minimalPullRequest).insertOnConflictUpdate(pr); + + final storedPr = await (db.select(db.minimalPullRequest) + ..where((pr) { + return pr.repoSlug.equals(slug.fullName) & pr.number.equals(number); + })) + .getSingle(); + _logger.v('Stored pull request: $slug#$number (${storedPr.head.sha})'); +} + +@riverpod +Future _fetchAndStorePullRequests(Ref ref, RepositorySlug slug) async { + final db = await ref.watch(databaseProvider.future); + + // Fetch new pull requests. + final prs = await ref.watch(_githubPullRequestsProvider(slug).future); + + // Update pull requests. + for (final pr in prs) { + await db.into(db.minimalPullRequest).insertOnConflictUpdate(pr); + } + + _logger.v('Stored ${prs.length} pull requests for $slug'); +} + +@riverpod +Future _githubPullRequest( + Ref ref, RepositorySlug slug, int number) async { + final client = await ref.watch(githubClientProvider.future); + if (client == null) { + throw StateError('No client available'); + } + + _logger.d('Fetching pull request $slug#$number'); + return client.getJSON( + '/repos/$slug/pulls/$number', + convert: _logger.catching(MinimalPullRequestData.fromJson), + statusCode: StatusCodes.OK, + ); +} + +@riverpod +Future> _githubPullRequests( + Ref ref, RepositorySlug slug) async { + final client = await ref.watch(githubClientProvider.future); + if (client == null) { + _logger.e('GitHub: no client available'); + return const []; + } + + _logger.v('GitHub: fetching pull requests for $slug'); + final prs = await PaginationHelper(client) + .objects, MinimalPullRequestData>( + 'GET', + 'repos/$slug/pulls', + _logger.catching(MinimalPullRequestData.fromJson), + statusCode: StatusCodes.OK, + ) + .map((cr) => cr.copyWith(repoSlug: Value(slug.fullName))) + .toList(); + _logger.d('GitHub: fetched ${prs.length} pull requests for $slug'); + return prs; +} diff --git a/lib/providers/repository.dart b/lib/providers/repository.dart deleted file mode 100644 index c3e6716..0000000 --- a/lib/providers/repository.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:github/github.dart' show PaginationHelper; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:tokhub/logger.dart'; -import 'package:tokhub/models/github.dart'; -import 'package:tokhub/objectbox.g.dart'; -import 'package:tokhub/providers/github.dart'; -import 'package:tokhub/providers/objectbox.dart'; -import 'package:tokhub/providers/pull_request.dart'; - -part 'repository.g.dart'; - -const _logger = Logger(['RepositoryProvider']); - -@riverpod -Future> repositories(Ref ref, - {required String org}) async { - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - final stored = box.query(MinimalRepository_.owner.equals(org)).build().find(); - if (stored.isNotEmpty) { - return stored; - } - await ref.watch(_fetchAndStoreRepositoriesProvider(org: org).future); - return box.query(MinimalRepository_.owner.equals(org)).build().find(); -} - -Future repositoriesRefresh(WidgetRef ref, {required String org}) async { - ref.invalidate(_fetchAndStoreRepositoriesProvider(org: org)); - await ref.watch(_fetchAndStoreRepositoriesProvider(org: org).future); -} - -@riverpod -Future> _fetchAndStoreRepositories(Ref ref, - {required String org}) async { - final repos = await ref.watch(_githubRepositoriesProvider(org: org).future); - final store = await ref.watch(objectBoxProvider.future); - final box = store.box(); - final ids = box.putMany(await Future.wait(repos.map((repo) async { - repo.pullRequests - .addAll(await ref.watch(pullRequestsProvider(repo).future)); - return repo; - }))); - _logger.v('Stored ${ids.length} repositories'); - return ids; -} - -@riverpod -Future> _githubRepositories(Ref ref, - {required String org}) async { - final client = await ref.watch(githubClientProvider.future); - if (client == null) { - return const []; - } - - _logger.d('Fetching repositories'); - return PaginationHelper(client) - .objects, MinimalRepository>( - 'GET', - '/orgs/$org/repos', - MinimalRepository.fromJson, - ) - .toList(); -} diff --git a/lib/widgets/home_view.dart b/lib/widgets/home_view.dart new file mode 100644 index 0000000..94b3fc6 --- /dev/null +++ b/lib/widgets/home_view.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tokhub/db/database.dart'; +import 'package:tokhub/providers/database.dart'; + +final class HomeView extends ConsumerWidget { + const HomeView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ref.watch(databaseProvider).when( + loading: () => const CircularProgressIndicator(), + error: (error, stacktrace) => Text('Error: $error\n$stacktrace'), + data: (data) => _build(data, ref)); + } + + Widget _build(Database db, WidgetRef ref) { + return FutureBuilder( + future: db.select(db.minimalRepository).get(), + builder: (context, snapshot) { + final repositories = snapshot.data; + if (snapshot.connectionState != ConnectionState.done || + repositories == null) { + return const CircularProgressIndicator(); + } + + return ListView.builder( + itemCount: repositories.length, + itemBuilder: (context, index) { + final repository = repositories[index]; + return ListTile( + title: Text(repository.name), + subtitle: Text(repository.fullName), + ); + }, + ); + }); + } +} diff --git a/lib/widgets/pull_request_tile.dart b/lib/widgets/pull_request_tile.dart index 679d34b..e4f8d10 100644 --- a/lib/widgets/pull_request_tile.dart +++ b/lib/widgets/pull_request_tile.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:tokhub/models/check_status.dart'; +import 'package:tokhub/models/github.dart'; import 'package:tokhub/models/pull_request_info.dart'; import 'package:tokhub/providers/check_status.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -21,9 +22,8 @@ final class PullRequestTile extends HookConsumerWidget { final textStyle = pullRequest.draft ? TextStyle(color: Colors.grey) : null; final force = useState(false); final checks = ref.watch(checkStatusProvider( - pullRequest.repo, - pullRequest.pr, - force: force.value, + pullRequest.repo.slug(), + pullRequest.pr.head.sha, )); return ExpansionTile( tilePadding: EdgeInsets.zero, @@ -140,11 +140,14 @@ final class PullRequestTile extends HookConsumerWidget { ], ), IconButton( - onPressed: () => checkStatusRefresh( - ref, - pullRequest.repo, - pullRequest.pr, - ), + onPressed: () async { + await checkStatusRefresh( + ref, + pullRequest.repo.slug(), + pullRequest.pr.head.sha, + pullRequest.pr.number, + ); + }, icon: Icon(Icons.refresh), ), ], diff --git a/lib/widgets/pull_requests_view.dart b/lib/widgets/pull_requests_view.dart index c392fa3..de31b57 100644 --- a/lib/widgets/pull_requests_view.dart +++ b/lib/widgets/pull_requests_view.dart @@ -1,14 +1,32 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tokhub/db/database.dart'; import 'package:tokhub/logger.dart'; import 'package:tokhub/models/github.dart'; import 'package:tokhub/models/pull_request_info.dart'; -import 'package:tokhub/providers/pull_request.dart'; -import 'package:tokhub/providers/repository.dart'; +import 'package:tokhub/providers/orgs/repos.dart'; +import 'package:tokhub/providers/repos/pulls.dart'; import 'package:tokhub/widgets/pull_request_tile.dart'; +part 'pull_requests_view.g.dart'; + const _logger = Logger(['PullRequestsView']); +@riverpod +Future)>> + reposWithPulls(Ref ref, String org, {bool includeEmpty = false}) async { + final repos = await ref.watch(repositoriesProvider(org).future); + final pairs = await Future.wait(repos.map((repo) async { + final prs = await ref.watch(pullRequestsProvider(repo.slug()).future); + return (repo, prs); + })); + if (includeEmpty) { + return pairs; + } + return pairs.where((pair) => pair.$2.isNotEmpty).toList(growable: false); +} + final class PullRequestsView extends ConsumerWidget { final String org; @@ -16,19 +34,15 @@ final class PullRequestsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ref.watch(repositoriesProvider(org: org)).when( + return ref.watch(reposWithPullsProvider(org)).when( loading: () => const CircularProgressIndicator(), error: (error, stacktrace) => Text('Error: $error\n$stacktrace'), data: (data) => _build(data, ref)); } - Widget _build(List repositories, WidgetRef ref) { - final reposWithPrs = repositories - .where((repo) => repo.pullRequests.isNotEmpty) - .toList(growable: false); - reposWithPrs.sort((a, b) => a.name.compareTo(b.name)); - _logger.v( - 'Building pull requests view with ${reposWithPrs.length} repositories'); + Widget _build( + List<(MinimalRepositoryData, List)> repos, + WidgetRef ref) { const columnWidths = { 0: FixedColumnWidth(48), 1: FixedColumnWidth(36), @@ -59,44 +73,40 @@ final class PullRequestsView extends ConsumerWidget { const Divider(), Expanded( child: ListView.builder( - itemCount: reposWithPrs.length, + itemCount: repos.length, itemBuilder: (context, index) { - final repo = reposWithPrs[index]; - final prs = repo.pullRequests - .map((spr) => PullRequestInfo.from(repo, spr)) - .toList(growable: false); - prs.sort((a, b) => b.number.compareTo(a.number)); - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), + final (repo, prs) = repos[index]; + return ExpansionTile( + enabled: prs.isNotEmpty, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Text(repo.name), + Text('${repo.name} (${prs.length})'), IconButton( icon: const Icon(Icons.refresh), onPressed: () async { - await pullRequestsRefresh(ref, repo); + _logger + .v('Refreshing pull requests for ${repo.slug()}'); + await pullRequestsRefresh(ref, repo.slug()); }, ), ], ), - subtitle: Column( - children: [ - for (final pr in prs) - ref.watch(pullRequestProvider(repo, pr.number)).when( - loading: () => PullRequestTile( - pullRequest: pr, - columnWidths: columnWidths, - ), - error: (error, stacktrace) => - Text('Error: $error $stacktrace'), - data: (spr) => PullRequestTile( - pullRequest: PullRequestInfo.from(repo, spr), - columnWidths: columnWidths, - ), + children: [ + for (final pr in prs) + ref.watch(pullRequestProvider(repo.slug(), pr.number)).when( + loading: () => PullRequestTile( + pullRequest: PullRequestInfo.from(repo, pr), + columnWidths: columnWidths, ), - ], - ), + error: (error, stacktrace) => + Text('Error: $error $stacktrace'), + data: (spr) => PullRequestTile( + pullRequest: PullRequestInfo.from(repo, spr), + columnWidths: columnWidths, + ), + ), + ], ); }, ), diff --git a/lib/widgets/repositories_view.dart b/lib/widgets/repositories_view.dart index 4b175bf..825bfcf 100644 --- a/lib/widgets/repositories_view.dart +++ b/lib/widgets/repositories_view.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tokhub/db/database.dart'; import 'package:tokhub/models/github.dart'; -import 'package:tokhub/models/pull_request_info.dart'; -import 'package:tokhub/providers/pull_request_info.dart'; +import 'package:tokhub/providers/orgs/repos.dart'; +import 'package:tokhub/providers/repos/pulls.dart'; final class RepositoriesView extends ConsumerWidget { final String org; @@ -11,23 +12,33 @@ final class RepositoriesView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ref.watch(pullRequestInfosProvider(org: org)).when( + return ref.watch(repositoriesProvider(org)).when( loading: () => const CircularProgressIndicator(), error: (error, stacktrace) => Text('Error: $error\n$stacktrace'), data: (data) => _build(data, ref)); } - Widget _build(List<(MinimalRepository, List)> repositories, - WidgetRef ref) { + Widget _build(List repos, WidgetRef ref) { return ListView.builder( - itemCount: repositories.length, + itemCount: repos.length, itemBuilder: (context, index) { - final (repo, prs) = repositories[index]; + final repository = repos[index]; return ListTile( - title: Text(repo.name), - subtitle: Text(repo.description ?? ''), - // Show number of pull requests. - trailing: Text('${prs.length} PRs'), + title: Text(repository.name), + subtitle: Text(repository.fullName), + trailing: FutureBuilder( + future: ref.watch(pullRequestsProvider(repository.slug()).future), + builder: (context, snapshot) { + final prs = snapshot.data ?? const []; + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + return Text('PRs: ${prs.length}'); + }, + ), ); }, ); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 72830e3..4c0025f 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,13 +6,13 @@ #include "generated_plugin_registrant.h" -#include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) objectbox_flutter_libs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "ObjectboxFlutterLibsPlugin"); - objectbox_flutter_libs_plugin_register_with_registrar(objectbox_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f869d60..ad279a8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - objectbox_flutter_libs + sqlite3_flutter_libs url_launcher_linux ) diff --git a/linux/my_application.cc b/linux/my_application.cc index 5b5acaf..b5c1d12 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -40,14 +40,14 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "tokhub"); + gtk_header_bar_set_title(header_bar, "TokHub"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "tokhub"); + gtk_window_set_title(window, "TokHub"); } - gtk_window_set_default_size(window, 1280, 720); + gtk_window_set_default_size(window, 480, 960); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0aeaeb3..c8c1561 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,12 @@ import FlutterMacOS import Foundation -import objectbox_flutter_libs import path_provider_foundation +import sqlite3_flutter_libs import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index efac454..6a1d3d7 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,40 +1,55 @@ PODS: - FlutterMacOS (1.0.0) - - ObjectBox (4.0.1) - - objectbox_flutter_libs (0.0.1): - - FlutterMacOS - - ObjectBox (= 4.0.1) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - sqlite3 (3.48.0): + - sqlite3/common (= 3.48.0) + - sqlite3/common (3.48.0) + - sqlite3/dbstatvtab (3.48.0): + - sqlite3/common + - sqlite3/fts5 (3.48.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.48.0): + - sqlite3/common + - sqlite3/rtree (3.48.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.48.0) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - url_launcher_macos (0.0.1): - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - - objectbox_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) SPEC REPOS: trunk: - - ObjectBox + - sqlite3 EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral - objectbox_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - ObjectBox: 0bc4bb75eea85f6af06b369148b334c2056bbc29 - objectbox_flutter_libs: 9d334e5c1008f69d6747d484afccf0860844a006 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + sqlite3: 3da10a59910c809fb584a93aa46a3f05b785e12e + sqlite3_flutter_libs: c26d86af4ad88f1465dc4e07e6dc6931eef228e4 url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 diff --git a/pubspec.lock b/pubspec.lock index 027c06a..79576ae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,31 +5,31 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "88399e291da5f7e889359681a8f64b18c5123e03576b01f32a6a276611e511c3" url: "https://pub.dev" source: hosted - version: "76.0.0" + version: "78.0.0" _macros: dependency: transitive description: dart source: sdk version: "0.3.3" analyzer: - dependency: transitive + dependency: "direct dev" description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "62899ef43d0b962b056ed2ebac6b47ec76ffd003d5f7c4e4dc870afe63188e33" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.1.0" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: "1d460d14e3c2ae36dc2b32cef847c4479198cf87704f63c3c3c8150ee50c3916" url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.12.0" archive: dependency: transitive description: @@ -134,6 +134,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -218,42 +226,58 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "3486c470bb93313a9417f926c7dd694a2e349220992d7b9d14534dc49c15bba9" + sha256: "6d509673c4dd0baa90e60dc8366bc2acc6690f16a7d44bfae31294d82c5d2a62" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.1" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "42cdc41994eeeddab0d7a722c7093ec52bd0761921eeb2cbdbf33d192a234759" + sha256: "8cc525c7b160eb47bb1ded8b2633c0f8b907930eb986ac577aded87cdd2835fe" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.1" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "02450c3e45e2a6e8b26c4d16687596ab3c4644dd5792e3313aa9ceba5a49b7f5" + sha256: "6dcee8a017181941c51a110da7e267c1d104dc74bec8862eeb8c85b5c8759a9e" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.1" custom_lint_visitor: dependency: transitive description: name: custom_lint_visitor - sha256: bfe9b7a09c4775a587b58d10ebb871d4fe618237639b1e84d5ec62d7dfef25f9 + sha256: "14df0760dfa81b7b0c398c876045f4e4a343eb2c9d200c66163671dd3e337c1b" url: "https://pub.dev" source: hosted - version: "1.0.0+6.11.0" + version: "1.0.0+7.1.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + drift: + dependency: "direct main" + description: + name: drift + sha256: "76f23535e19a9f2be92f954e74d8802e96f526e5195d7408c1a20f6659043941" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.24.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: d1d90b0d55b22de412b77186f3bf3179a4b7e2acc4c8fb3a7aaf28a01abc194b + url: "https://pub.dev" + source: hosted + version: "2.24.0" fake_async: dependency: transitive description: @@ -286,14 +310,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - flat_buffers: - dependency: transitive - description: - name: flat_buffers - sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3" - url: "https://pub.dev" - source: hosted - version: "23.5.26" flutter: dependency: "direct main" description: flutter @@ -311,10 +327,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5" + sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c url: "https://pub.dev" source: hosted - version: "0.14.2" + version: "0.14.3" flutter_lints: dependency: "direct dev" description: @@ -345,10 +361,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -401,18 +417,18 @@ packages: dependency: transitive description: name: hotreloader - sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" http: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: @@ -425,10 +441,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" image: dependency: transitive description: @@ -465,10 +481,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c + sha256: b0a98230538fe5d0b60a22fb6bf1b6cb03471b53e3324ff6069c591679dd59c9 url: "https://pub.dev" source: hosted - version: "6.9.0" + version: "6.9.3" leak_tracker: dependency: transitive description: @@ -549,30 +565,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - objectbox: - dependency: "direct main" - description: - name: objectbox - sha256: ea823f4bf1d0a636e7aa50b43daabb64dd0fbd80b85a033016ccc1bc4f76f432 - url: "https://pub.dev" - source: hosted - version: "4.0.3" - objectbox_flutter_libs: - dependency: "direct main" - description: - name: objectbox_flutter_libs - sha256: c91350bbbce5e6c2038255760b5be988faead004c814f833c2cd137445c6ae70 - url: "https://pub.dev" - source: hosted - version: "4.0.3" - objectbox_generator: - dependency: "direct dev" - description: - name: objectbox_generator - sha256: "96da521f2cef455cd524f8854e31d64495c50711ad5f1e2cf3142a8e527bc75f" - url: "https://pub.dev" - source: hosted - version: "4.0.3" package_config: dependency: transitive description: @@ -661,14 +653,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.dev" - source: hosted - version: "3.9.1" pool: dependency: transitive description: @@ -697,10 +681,18 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" riverpod: dependency: transitive description: @@ -713,10 +705,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: c6b8222b2b483cb87ae77ad147d6408f400c64f060df7a225b127f4afef4f8c8 + sha256: "837a6dc33f490706c7f4632c516bcd10804ee4d9ccc8046124ca56388715fdf3" url: "https://pub.dev" source: hosted - version: "0.5.8" + version: "0.5.9" riverpod_annotation: dependency: "direct main" description: @@ -729,18 +721,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "63546d70952015f0981361636bf8f356d9cfd9d7f6f0815e3c07789a41233188" + sha256: "120d3310f687f43e7011bb213b90a436f1bbc300f0e4b251a72c39bccb017a4f" url: "https://pub.dev" source: hosted - version: "2.6.3" + version: "2.6.4" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "83e4caa337a9840469b7b9bd8c2351ce85abad80f570d84146911b32086fbd99" + sha256: b05408412b0f75dec954e032c855bc28349eeed2d2187f94519e1ddfdf8b3693 url: "https://pub.dev" source: hosted - version: "2.6.3" + version: "2.6.4" rxdart: dependency: transitive description: @@ -774,10 +766,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_helper: dependency: transitive description: @@ -802,6 +794,30 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "35d3726fe18ab1463403a5cc8d97dbc81f2a0b08082e8173851363fcc97b6627" + url: "https://pub.dev" + source: hosted + version: "2.7.2" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "50a7e3f294c741d3142eed0ff228e38498334e11e0ccb9d73e0496e005949e44" + url: "https://pub.dev" + source: hosted + version: "0.5.29" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee" + url: "https://pub.dev" + source: hosted + version: "0.41.0" stack_trace: dependency: transitive description: @@ -926,18 +942,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" uuid: dependency: transitive description: @@ -1020,4 +1036,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index a80dbdf..2d186e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: tokhub -description: "A new Flutter project." +description: "TokTok GitHub companion." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -34,20 +34,21 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + + circular_buffer: ^0.12.0 + clock: ^1.1.1 + drift: ^2.23.1 flutter_hooks: ^0.20.5 flutter_riverpod: ^2.6.1 freezed_annotation: ^2.4.4 github: ^9.24.0 + hooks_riverpod: ^2.6.1 json_annotation: ^4.9.0 - objectbox_flutter_libs: ^4.0.3 - objectbox: ^4.0.3 path_provider: ^2.1.5 path: ^1.9.0 riverpod_annotation: ^2.6.1 + sqlite3_flutter_libs: ^0.5.29 url_launcher: ^6.3.1 - clock: ^1.1.1 - hooks_riverpod: ^2.6.1 - circular_buffer: ^0.12.0 dev_dependencies: flutter_test: @@ -58,13 +59,14 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. + analyzer: ^7.1.0 build_runner: ^2.4.13 custom_lint: ^0.7.0 + drift_dev: ^2.23.1 flutter_launcher_icons: "^0.14.1" flutter_lints: ^5.0.0 freezed: ^2.5.7 json_serializable: ^6.8.0 - objectbox_generator: ^4.0.3 riverpod_generator: ^2.6.2 riverpod_lint: ^2.6.2 @@ -81,6 +83,7 @@ flutter_launcher_icons: # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in diff --git a/tools/prepare b/tools/prepare index 758501f..270a83e 100755 --- a/tools/prepare +++ b/tools/prepare @@ -2,6 +2,9 @@ set -eux +# Upgrade versions. +flutter pub upgrade --major-versions + # Make sure we have all packages downloaded. flutter pub get diff --git a/tools/prepare-web b/tools/prepare-web new file mode 100755 index 0000000..5c441e1 --- /dev/null +++ b/tools/prepare-web @@ -0,0 +1,13 @@ +#!/bin/sh + +set -eux + +./tools/prepare + +# SQLite wasm/js. +SQLITE_VERSION=2.6.1 +curl -L "https://github.com/simolus3/sqlite3.dart/releases/download/sqlite3-$SQLITE_VERSION/sqlite3.wasm" -o web/sqlite3.wasm + +# Drift worker. +DRIFT_VERSION=2.23.1 +curl -L "https://github.com/simolus3/drift/releases/download/drift-$DRIFT_VERSION/drift_worker.js" -o web/drift_worker.js diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..7e4f0ed --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,2 @@ +/*.wasm +/drift_worker.js diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 67c8ce3..76d5285 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,12 @@ #include "generated_plugin_registrant.h" -#include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - ObjectboxFlutterLibsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8d9ea83..22aeaae 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - objectbox_flutter_libs + sqlite3_flutter_libs url_launcher_windows )