diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 22574fdb..cecfef4b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -219,7 +219,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4036a781..6e6953dc 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ > supportedLanguages = [ {"name": "English", "languageCode": "en", "countryCode": "US"}, {"name": "Français ", "languageCode": "fr", "countryCode": "FR"}, + {"name": "Traditional Chinese ", "languageCode": "zh", "countryCode": "TW"}, ]; class AppTranslations extends Translations { diff --git a/lib/src/core/utils/twitch_event_sub.dart b/lib/src/core/utils/twitch_event_sub.dart index dc167442..fc8ef5e4 100644 --- a/lib/src/core/utils/twitch_event_sub.dart +++ b/lib/src/core/utils/twitch_event_sub.dart @@ -51,10 +51,10 @@ class TwitchEventSub { } void _eventListener(String data) { - debugPrint("Sub event: $data"); + // debugPrint("Sub event: $data"); Map msgMapped = jsonDecode(data); - if (msgMapped['metadata']['message_type'] == 'session_welcome') { + if (msgMapped['metadata'] != null && msgMapped['metadata']['message_type'] == 'session_welcome') { String sessionId = msgMapped['payload']['session']['id']; //SUBSCRIBE TO POLLS BEGIN, PROGRESS, END @@ -86,8 +86,8 @@ class TwitchEventSub { fakeData(); } - if (msgMapped['metadata']['subscription'] != null) { - switch (msgMapped['metadata']['subscription']['type']) { + if (msgMapped['subscription'] != null) { + switch (msgMapped['subscription']['type']) { //POLLS case 'channel.poll.begin': currentPoll.value = TwitchPollDTO.fromJson(msgMapped['event']); @@ -162,7 +162,512 @@ class TwitchEventSub { } } - void fakeData(){ + void fakeData() async { + // // PREDICTIONS + // _eventListener(predictionBeginJson); + // await Future.delayed(const Duration(seconds: 10)); + // _eventListener(predictionProgressJson); + // await Future.delayed(const Duration(seconds: 10)); + // _eventListener(predictionLockJson); + // await Future.delayed(const Duration(seconds: 10)); + // _eventListener(predictionEndJson); + // // POLLS + // _eventListener(pollBeginJson); + // await Future.delayed(const Duration(seconds: 10)); + // _eventListener(pollProgressJson); + // await Future.delayed(const Duration(seconds: 10)); + // _eventListener(pollEndJson); + + // HYPE TRAIN + // _eventListener(hypeBeginJson); + // await Future.delayed(const Duration(seconds: 4)); + // _eventListener(hypeProgressJson); + // await Future.delayed(const Duration(seconds: 4)); + // _eventListener(hypeEndJson); } } + +const String predictionBeginJson = ''' + { + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.prediction.begin", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1243456", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "title": "Aren’t shoes just really hard socks?", + "outcomes": [ + {"id": "1243456", "title": "Yeah!", "color": "blue"}, + {"id": "2243456", "title": "No!", "color": "pink"} + ], + "started_at": "2020-07-15T17:16:03.17106713Z", + "locks_at": "2020-07-15T17:21:03.17106713Z" + } +} +'''; + +const String predictionProgressJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.prediction.progress", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1243456", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "title": "Aren’t shoes just really hard socks?", + "outcomes": [ + { + "id": "1243456", + "title": "Yeah!", + "color": "blue", + "users": 10, + "channel_points": 15000, + "top_predictors": [ + { + "user_name": "Cool_User", + "user_login": "cool_user", + "user_id": "1234", + "channel_points_won": null, + "channel_points_used": 500 + }, + { + "user_name": "Coolest_User", + "user_login": "coolest_user", + "user_id": "1236", + "channel_points_won": null, + "channel_points_used": 200 + } + ] + }, + { + "id": "2243456", + "title": "No!", + "color": "pink", + "top_predictors": [ + { + "user_name": "Cooler_User", + "user_login": "cooler_user", + "user_id": 12345, + "channel_points_won": null, + "channel_points_used": 5000 + } + ] + } + ], + "started_at": "2020-07-15T17:16:03.17106713Z", + "locks_at": "2023-08-18T19:21:03.17106713Z" + } +} +'''; + +const String predictionLockJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.prediction.lock", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1243456", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "title": "Aren’t shoes just really hard socks?", + "outcomes": [ + { + "id": "1243456", + "title": "Yeah!", + "color": "blue", + "users": 10, + "channel_points": 15000, + "top_predictors": [ + { + "user_name": "Cool_User", + "user_login": "cool_user", + "user_id": "1234", + "channel_points_won": null, + "channel_points_used": 500 + }, + { + "user_name": "Coolest_User", + "user_login": "coolest_user", + "user_id": "1236", + "channel_points_won": null, + "channel_points_used": 200 + } + ] + }, + { + "id": "2243456", + "title": "No!", + "color": "pink", + "top_predictors": [ + { + "user_name": "Cooler_User", + "user_login": "cooler_user", + "user_id": 12345, + "channel_points_won": null, + "channel_points_used": 5000 + } + ] + } + ], + "started_at": "2020-07-15T17:16:03.17106713Z", + "locked_at": "2020-07-15T17:21:03.17106713Z" + } +} +'''; + +const String predictionEndJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.prediction.end", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1243456", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "title": "Aren’t shoes just really hard socks?", + "winning_outcome_id": "12345", + "outcomes": [ + { + "id": "12345", + "title": "Yeah!", + "color": "blue", + "users": 2, + "channel_points": 15000, + "top_predictors": [ + { + "user_name": "Cool_User", + "user_login": "cool_user", + "user_id": "1234", + "channel_points_won": 10000, + "channel_points_used": 500 + }, + { + "user_name": "Coolest_User", + "user_login": "coolest_user", + "user_id": "1236", + "channel_points_won": 5000, + "channel_points_used": 100 + } + ] + }, + { + "id": "22435", + "title": "No!", + "users": 2, + "channel_points": 200, + "color": "pink", + "top_predictors": [ + { + "user_name": "Cooler_User", + "user_login": "cooler_user", + "user_id": 12345, + "channel_points_won": null, + "channel_points_used": 100 + }, + { + "user_name": "Elite_User", + "user_login": "elite_user", + "user_id": 1337, + "channel_points_won": null, + "channel_points_used": 100 + } + ] + } + ], + "status": "resolved", + "started_at": "2020-07-15T17:16:03.17106713Z", + "ended_at": "2020-07-15T17:16:11.17106713Z" + } +} +'''; + +const String pollBeginJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.poll.begin", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1243456", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "title": "Aren’t shoes just really hard socks?", + "choices": [ + {"id": "123", "title": "Yeah!"}, + {"id": "124", "title": "No!"}, + {"id": "125", "title": "Maybe!"} + ], + "bits_voting": { + "is_enabled": true, + "amount_per_vote": 10 + }, + "channel_points_voting": { + "is_enabled": true, + "amount_per_vote": 10 + }, + "started_at": "2020-07-15T17:16:03.17106713Z", + "ends_at": "2020-07-15T17:16:08.17106713Z" + } +} +'''; + +const String pollProgressJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.poll.progress", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1243456", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "title": "Aren’t shoes just really hard socks?", + "choices": [ + {"id": "123", "title": "Yeah!", "bits_votes": 5, "channel_points_votes": 7, "votes": 12}, + {"id": "124", "title": "No!", "bits_votes": 10, "channel_points_votes": 4, "votes": 14}, + {"id": "125", "title": "Maybe!", "bits_votes": 0, "channel_points_votes": 7, "votes": 7} + ], + "bits_voting": { + "is_enabled": true, + "amount_per_vote": 10 + }, + "channel_points_voting": { + "is_enabled": true, + "amount_per_vote": 10 + }, + "started_at": "2020-07-15T17:16:03.17106713Z", + "ends_at": "2020-07-15T17:16:08.17106713Z" + } +} +'''; + +const String pollEndJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.poll.end", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1243456", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "title": "Aren’t shoes just really hard socks?", + "choices": [ + {"id": "123", "title": "Blue", "bits_votes": 50, "channel_points_votes": 70, "votes": 120}, + {"id": "124", "title": "Yellow", "bits_votes": 100, "channel_points_votes": 40, "votes": 140}, + {"id": "125", "title": "Green", "bits_votes": 10, "channel_points_votes": 70, "votes": 80} + ], + "bits_voting": { + "is_enabled": true, + "amount_per_vote": 10 + }, + "channel_points_voting": { + "is_enabled": true, + "amount_per_vote": 10 + }, + "status": "completed", + "started_at": "2020-07-15T17:16:03.17106713Z", + "ended_at": "2020-07-15T17:16:11.17106713Z" + } +} +'''; + +const String hypeBeginJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.hype_train.begin", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1b0AsbInCHZW2SQFQkCzqN07Ib2", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "total": 137, + "progress": 137, + "goal": 500, + "top_contributions": [ + { "user_id": "123", "user_login": "pogchamp", "user_name": "PogChamp", "type": "bits", "total": 50 }, + { "user_id": "456", "user_login": "kappa", "user_name": "Kappa", "type": "subscription", "total": 45 } + ], + "last_contribution": { "user_id": "123", "user_login": "pogchamp", "user_name": "PogChamp", "type": "bits", "total": 50 }, + "level": 2, + "started_at": "2020-07-15T17:16:03.17106713Z", + "expires_at": "2023-08-18T06:21:03.17106713Z" + } +} +'''; + +const String hypeProgressJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.hype_train.progress", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1b0AsbInCHZW2SQFQkCzqN07Ib2", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "level": 2, + "total": 700, + "progress": 200, + "goal": 1000, + "top_contributions": [ + { "user_id": "123", "user_login": "pogchamp", "user_name": "PogChamp", "type": "bits", "total": 50 }, + { "user_id": "456", "user_login": "kappa", "user_name": "Kappa", "type": "subscription", "total": 45 } + ], + "last_contribution": { "user_id": "123", "user_login": "pogchamp", "user_name": "PogChamp", "type": "bits", "total": 50 }, + "started_at": "2020-07-15T17:16:03.17106713Z", + "expires_at": "2023-08-18T06:21:03.17106713Z" + } +} +'''; + +const String hypeEndJson = ''' +{ + "subscription": { + "id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", + "type": "channel.hype_train.end", + "version": "1", + "status": "enabled", + "cost": 0, + "condition": { + "broadcaster_user_id": "1337" + }, + "transport": { + "method": "webhook", + "callback": "https://example.com/webhooks/callback" + }, + "created_at": "2019-11-16T10:11:12.634234626Z" + }, + "event": { + "id": "1b0AsbInCHZW2SQFQkCzqN07Ib2", + "broadcaster_user_id": "1337", + "broadcaster_user_login": "cool_user", + "broadcaster_user_name": "Cool_User", + "level": 2, + "total": 137, + "top_contributions": [ + { "user_id": "123", "user_login": "pogchamp", "user_name": "PogChamp", "type": "bits", "total": 50 }, + { "user_id": "456", "user_login": "kappa", "user_name": "Kappa", "type": "subscription", "total": 45 } + ], + "started_at": "2020-07-15T17:16:03.17106713Z", + "ended_at": "2023-08-18T04:35:03.17106713Z", + "cooldown_ends_at": "2020-07-15T18:16:11.17106713Z" + } +} +'''; \ No newline at end of file diff --git a/lib/src/data/entities/twitch/twitch_hype_train_dto.dart b/lib/src/data/entities/twitch/twitch_hype_train_dto.dart index a302afc4..8a1199b5 100644 --- a/lib/src/data/entities/twitch/twitch_hype_train_dto.dart +++ b/lib/src/data/entities/twitch/twitch_hype_train_dto.dart @@ -1,3 +1,5 @@ +import 'package:intl/intl.dart'; + import '../../../domain/entities/twitch/twitch_hype_train.dart'; class TwitchHypeTrainDTO extends TwitchHypeTrain { @@ -8,7 +10,8 @@ class TwitchHypeTrainDTO extends TwitchHypeTrain { required int goal, required int level, required List topContributions, - required Contribution lastContribution, + required Contribution? lastContribution, + required Duration endsAt, }) : super( id: id, total: total, @@ -17,6 +20,7 @@ class TwitchHypeTrainDTO extends TwitchHypeTrain { level: level, topContributions: topContributions, lastContribution: lastContribution, + endsAt: endsAt, ); @override @@ -28,6 +32,7 @@ class TwitchHypeTrainDTO extends TwitchHypeTrain { 'level': level, 'topContributions': topContributions, 'lastContribution': lastContribution, + 'endsAt': endsAt, }; factory TwitchHypeTrainDTO.fromJson(Map map) { @@ -39,14 +44,22 @@ class TwitchHypeTrainDTO extends TwitchHypeTrain { topContributions.add(c), }); + DateFormat df = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + DateTime endsAt = df.parse(map['expires_at'] ?? map['ended_at']); + DateTime now = df.parse(df.format(DateTime.now().toUtc())); + Duration endsAtDuration = endsAt.difference(now); + return TwitchHypeTrainDTO( id: map['id'], total: map['total'], - progress: map['progress'], - goal: map['goal'], + progress: map['progress'] ?? 0, + goal: map['goal'] ?? 0, level: map['level'], topContributions: topContributions, - lastContribution: ContributionDTO.fromJson(map['last_contribution']), + lastContribution: map['last_contribution'] != null + ? ContributionDTO.fromJson(map['last_contribution']) + : null, + endsAt: endsAtDuration, ); } } diff --git a/lib/src/data/entities/twitch_poll_dto.dart b/lib/src/data/entities/twitch_poll_dto.dart index 9ce78c9a..0d20a7c8 100644 --- a/lib/src/data/entities/twitch_poll_dto.dart +++ b/lib/src/data/entities/twitch_poll_dto.dart @@ -71,7 +71,7 @@ class ChoiceDTO extends Choice { return ChoiceDTO( id: map['id'], title: map['title'], - votes: map['votes'], + votes: map['votes'] ?? 0, ); } } diff --git a/lib/src/data/entities/twitch_prediction_dto.dart b/lib/src/data/entities/twitch_prediction_dto.dart index 2a510ad0..7406b14b 100644 --- a/lib/src/data/entities/twitch_prediction_dto.dart +++ b/lib/src/data/entities/twitch_prediction_dto.dart @@ -40,6 +40,10 @@ class TwitchPredictionDTO extends TwitchPrediction { totalUsers += o.users, }); + if(map['locked_at'] != null){ + status = PredictionStatus.locked; + } + switch (map["status"]) { case "resolved": status = PredictionStatus.resolved; @@ -92,8 +96,8 @@ class OutcomeDTO extends Outcome { return OutcomeDTO( id: map['id'], title: map['title'], - users: int.parse(map['users']), - channelPoints: int.parse(map['channel_points']), + users: map['users'] ?? 0, + channelPoints: map['channel_points'] ?? 0, color: color, ); } diff --git a/lib/src/domain/entities/twitch/twitch_hype_train.dart b/lib/src/domain/entities/twitch/twitch_hype_train.dart index 0231baf7..55b91f8f 100644 --- a/lib/src/domain/entities/twitch/twitch_hype_train.dart +++ b/lib/src/domain/entities/twitch/twitch_hype_train.dart @@ -7,7 +7,8 @@ class TwitchHypeTrain extends Equatable { final int goal; final int level; final List topContributions; - final Contribution lastContribution; + final Contribution? lastContribution; + final Duration endsAt; const TwitchHypeTrain({ required this.id, @@ -17,6 +18,7 @@ class TwitchHypeTrain extends Equatable { required this.level, required this.topContributions, required this.lastContribution, + required this.endsAt, }); Map toJson() => { @@ -27,6 +29,7 @@ class TwitchHypeTrain extends Equatable { 'level': level, 'topContributions': topContributions, 'lastContribution': lastContribution, + 'endsAt': endsAt, }; @override @@ -39,6 +42,7 @@ class TwitchHypeTrain extends Equatable { level, topContributions, lastContribution, + endsAt, ]; } diff --git a/lib/src/presentation/controllers/twitch_tab_view_controller.dart b/lib/src/presentation/controllers/twitch_tab_view_controller.dart index 7f76fdc0..739b4b0e 100644 --- a/lib/src/presentation/controllers/twitch_tab_view_controller.dart +++ b/lib/src/presentation/controllers/twitch_tab_view_controller.dart @@ -29,7 +29,7 @@ class TwitchTabViewController extends GetxController { Timer? refreshDataTimerProgressBar; Rx myDuration = const Duration(seconds: 15).obs; - late TwitchEventSub twitchEventSub; + TwitchEventSub? twitchEventSub; @override void onInit() { @@ -45,11 +45,11 @@ class TwitchTabViewController extends GetxController { refreshDataTimer = Timer.periodic(const Duration(seconds: 15), (timer) { refreshData(); }); - // twitchEventSub = TwitchEventSub( - // homeViewController.twitchData!.twitchUser.login, - // homeViewController.twitchData!.accessToken, - // ); - // twitchEventSub.connect(); + twitchEventSub = TwitchEventSub( + homeViewController.twitchData!.twitchUser.login, + homeViewController.twitchData!.accessToken, + ); + twitchEventSub!.connect(); } refreshDataTimerProgressBar = @@ -137,7 +137,7 @@ class TwitchTabViewController extends GetxController { homeEvents.endPoll( homeViewController.twitchData!.accessToken, homeViewController.twitchData!.twitchUser.id, - twitchEventSub.currentPoll.value!.id, + twitchEventSub!.currentPoll.value!.id, status); } diff --git a/lib/src/presentation/views/home_view.dart b/lib/src/presentation/views/home_view.dart index ec2b2e41..08b49946 100644 --- a/lib/src/presentation/views/home_view.dart +++ b/lib/src/presentation/views/home_view.dart @@ -6,9 +6,11 @@ import 'package:get/get.dart'; import 'package:irllink/routes/app_routes.dart'; import 'package:irllink/src/presentation/controllers/chat_view_controller.dart'; import 'package:irllink/src/presentation/controllers/home_view_controller.dart'; +import 'package:irllink/src/presentation/controllers/twitch_tab_view_controller.dart'; import 'package:irllink/src/presentation/widgets/chat_view.dart'; import 'package:irllink/src/presentation/widgets/dashboard.dart'; import 'package:irllink/src/presentation/widgets/emote_picker_view.dart'; +import 'package:irllink/src/presentation/widgets/hype_train.dart'; import 'package:irllink/src/presentation/widgets/tabs/obs_tab_view.dart'; import 'package:irllink/src/presentation/widgets/tabs/streamelements_tab_view.dart'; import 'package:irllink/src/presentation/widgets/tabs/twitch_tab_view.dart'; @@ -160,6 +162,12 @@ class HomeView extends GetView { child: controller.channels.isNotEmpty ? Column( children: [ + Padding( + padding: const EdgeInsets.only( + left: 8, right: 8, top: 4, bottom: 0), + child: hypeTrain( + context, Get.find()), + ), Visibility( visible: controller.channels.length > 1, child: _tabBarChats(context), @@ -272,9 +280,12 @@ class HomeView extends GetView { color: Theme.of(context) .textTheme .bodyLarge! - .color, + .backgroundColor, fontSize: 16), - hintText: 'send_message'.tr, + hintText: controller.settings.value.generalSettings! + .displayViewerCount + ? '${Get.find().twitchStreamInfos.value.viewerCount} viewers' + : 'send_message'.tr, isDense: true, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, @@ -325,7 +336,8 @@ class HomeView extends GetView { controller.chatTabsController .animateTo(controller.selectedChatIndex!); } - if (controller.tabIndex.value > controller.tabElements.length - 1) { + if (controller.tabIndex.value > + controller.tabElements.length - 1) { controller.tabIndex.value = 0; controller.tabController.animateTo(controller.tabIndex.value); } else { diff --git a/lib/src/presentation/widgets/hype_train.dart b/lib/src/presentation/widgets/hype_train.dart new file mode 100644 index 00000000..4a2ca91c --- /dev/null +++ b/lib/src/presentation/widgets/hype_train.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:irllink/src/presentation/controllers/twitch_tab_view_controller.dart'; + +Widget hypeTrain( + BuildContext context, + TwitchTabViewController controller, +) { + return controller.twitchEventSub?.currentHypeTrain.value != null + ? ValueListenableBuilder( + valueListenable: controller.twitchEventSub!.currentHypeTrain, + builder: (context, hypetrain, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Container( + padding: const EdgeInsets.only( + left: 8, right: 8, top: 2, bottom: 2), + decoration: const BoxDecoration( + color: Colors.deepPurple, + borderRadius: BorderRadius.all( + Radius.circular(8), + ), + ), + child: Text('LVL ${hypetrain!.level}'), + ), + const SizedBox( + width: 10, + ), + const Text('Hype Train'), + ], + ), + Text('${hypetrain.progress}%'), + Text(_printDuration(hypetrain.endsAt)) + ], + ); + }, + ) + : Container(); +} + +String _printDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, "0"); + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); + return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds"; +} diff --git a/lib/src/presentation/widgets/tabs/twitch_tab_view.dart b/lib/src/presentation/widgets/tabs/twitch_tab_view.dart index 5100482c..ce77af99 100644 --- a/lib/src/presentation/widgets/tabs/twitch_tab_view.dart +++ b/lib/src/presentation/widgets/tabs/twitch_tab_view.dart @@ -21,7 +21,7 @@ class TwitchTabView extends GetView { child: SingleChildScrollView( child: Obx( () => Container( - padding: const EdgeInsets.only(left: 20.0, top: 12.0, right: 20.0), + padding: const EdgeInsets.only(left: 20.0, top: 12.0, right: 20.0, bottom: 12.0), color: Theme.of(context).colorScheme.background, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -242,22 +242,22 @@ class TwitchTabView extends GetView { context: context, isOn: false, ), - // Divider( - // height: 40, - // thickness: 4, - // indent: 0, - // endIndent: 0, - // color: Theme.of(context).colorScheme.secondary, - // ), - // _prediction(context, controller), - // Divider( - // height: 40, - // thickness: 4, - // indent: 0, - // endIndent: 0, - // color: Theme.of(context).colorScheme.secondary, - // ), - // _poll(context, controller), + Divider( + height: 40, + thickness: 4, + indent: 0, + endIndent: 0, + color: Theme.of(context).colorScheme.secondary, + ), + prediction(context, controller), + Divider( + height: 40, + thickness: 4, + indent: 0, + endIndent: 0, + color: Theme.of(context).colorScheme.secondary, + ), + poll(context, controller), ], ), ), @@ -271,300 +271,320 @@ Widget prediction( BuildContext context, TwitchTabViewController controller, ) { - return ValueListenableBuilder( - valueListenable: controller.twitchEventSub.currentPrediction, - builder: (context, prediction, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Prediction", - style: TextStyle( - color: Theme.of(Get.context!).textTheme.bodyLarge!.color, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - prediction != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(prediction.title), - const SizedBox(height: 10), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: prediction.outcomes.length, - itemBuilder: (context, index) { - final outcome = prediction.outcomes[index]; - final percentage = - outcome.users / prediction.totalUsers; - return Visibility( - visible: (prediction.status == - PredictionStatus.active || - prediction.status == PredictionStatus.locked || - (prediction.status == PredictionStatus.resolved && - outcome.id == prediction.winningOutcomeId)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - prediction.status == PredictionStatus.resolved - ? "Winner: ${outcome.title}" - : outcome.title, - style: TextStyle( - color: Theme.of(Get.context!) - .textTheme - .bodyLarge! - .color, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 3), - LinearPercentIndicator( - animation: true, - animateFromLastPercent: true, - barRadius: const Radius.circular(8), - padding: - const EdgeInsets.symmetric(horizontal: 0.0), - lineHeight: 20.0, - percent: percentage, - backgroundColor: - Theme.of(context).colorScheme.secondary, - progressColor: outcome.color, - center: Text( - "${(percentage * 100).toStringAsFixed(2)} %"), - ), - const SizedBox(height: 10), - ], - ), - ); - }, - ), - Visibility( - visible: prediction.status != PredictionStatus.canceled, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return controller.twitchEventSub?.currentPrediction.value != null + ? ValueListenableBuilder( + valueListenable: controller.twitchEventSub!.currentPrediction, + builder: (context, prediction, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Prediction", + style: TextStyle( + color: Theme.of(Get.context!).textTheme.bodyLarge!.color, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + prediction != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextButton( - style: TextButton.styleFrom( - textStyle: const TextStyle(fontSize: 12), - backgroundColor: Theme.of(context) - .colorScheme - .tertiaryContainer, - ), - onPressed: () { - controller.endPrediction("CANCELED", null); + Text(prediction.title), + const SizedBox(height: 10), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: prediction.outcomes.length, + itemBuilder: (context, index) { + final outcome = prediction.outcomes[index]; + final percentage = prediction.totalUsers > 0 + ? outcome.users / prediction.totalUsers + : 0.0; + return Visibility( + visible: (prediction.status == + PredictionStatus.active || + prediction.status == + PredictionStatus.locked || + (prediction.status == + PredictionStatus.resolved && + outcome.id == + prediction.winningOutcomeId)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + prediction.status == + PredictionStatus.resolved + ? "Winner: ${outcome.title}" + : outcome.title, + style: TextStyle( + color: Theme.of(Get.context!) + .textTheme + .bodyLarge! + .color, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 3), + LinearPercentIndicator( + animation: true, + animateFromLastPercent: true, + barRadius: const Radius.circular(8), + padding: const EdgeInsets.symmetric( + horizontal: 0.0), + lineHeight: 20.0, + percent: percentage, + backgroundColor: Theme.of(context) + .colorScheme + .secondary, + progressColor: outcome.color, + center: Text( + "${(percentage * 100).toStringAsFixed(2)} %"), + ), + const SizedBox(height: 10), + ], + ), + ); }, - child: Text( - "cancel".tr, - style: const TextStyle( - color: Colors.white, - ), - ), ), - TextButton( - style: TextButton.styleFrom( - textStyle: const TextStyle(fontSize: 12), - backgroundColor: Colors.green, - ), - onPressed: () { - prediction.status == PredictionStatus.active - ? controller.endPrediction("LOCKED", null) - : pickWinnerDialog(context, prediction, - controller.endPrediction, controller); - }, - child: Text( - prediction.status == PredictionStatus.active - ? 'Lock' - : 'End', - style: const TextStyle( - color: Colors.white, - ), + Visibility( + visible: prediction.status != + PredictionStatus.resolved && + prediction.status != PredictionStatus.canceled, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 12), + backgroundColor: Theme.of(context) + .colorScheme + .tertiaryContainer, + ), + onPressed: () { + controller.endPrediction("CANCELED", null); + }, + child: Text( + "cancel".tr, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + TextButton( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 12), + backgroundColor: Colors.green, + ), + onPressed: () { + prediction.status == PredictionStatus.active + ? controller.endPrediction( + "LOCKED", null) + : pickWinnerDialog( + context, + prediction, + controller.endPrediction, + controller); + }, + child: Text( + prediction.status == PredictionStatus.active + ? 'Lock' + : 'End', + style: const TextStyle( + color: Colors.white, + ), + ), + ), + ], ), ), ], - ), - ), - ], - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "No prediction running", - style: TextStyle( - color: Theme.of(context).textTheme.bodyLarge!.color, - ), - ), - TextButton( - style: TextButton.styleFrom( - textStyle: const TextStyle(fontSize: 12), - backgroundColor: Colors.deepPurpleAccent, - ), - onPressed: () {}, - child: const Text( - 'Create one', - style: TextStyle( - color: Colors.white, - ), - ), - ), - ], - ), - ], - ); - }, - ); + ) + : Container() + ], + ); + }, + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "No prediction running", + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + ), + ), + // TextButton( + // style: TextButton.styleFrom( + // textStyle: const TextStyle(fontSize: 12), + // backgroundColor: Colors.deepPurpleAccent, + // ), + // onPressed: () {}, + // child: const Text( + // 'Create one', + // style: TextStyle( + // color: Colors.white, + // ), + // ), + // ), + ], + ); } Widget poll( BuildContext context, TwitchTabViewController controller, ) { - return ValueListenableBuilder( - valueListenable: controller.twitchEventSub.currentPoll, - builder: (context, poll, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Poll", - style: TextStyle( - color: Theme.of(Get.context!).textTheme.bodyLarge!.color, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - poll != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(poll.title), - const SizedBox(height: 10), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: poll.choices.length, - itemBuilder: (context, index) { - final choice = poll.choices[index]; - final percentage = choice.votes / poll.totalVotes; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - choice.title, - style: TextStyle( - color: Theme.of(Get.context!) - .textTheme - .bodyLarge! - .color, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 3), - LinearPercentIndicator( - animation: true, - animateFromLastPercent: true, - barRadius: const Radius.circular(8), - padding: - const EdgeInsets.symmetric(horizontal: 0.0), - lineHeight: 20.0, - percent: percentage, - backgroundColor: - Theme.of(context).colorScheme.secondary, - progressColor: ((poll.status == - PollStatus.completed) && - percentage > 0.5) - ? Colors.green - : Theme.of(context).colorScheme.background, - center: Text( - "${(percentage * 100).toStringAsFixed(2)} %"), - ), - const SizedBox(height: 10), - ], - ); - }, - ), - Visibility( - visible: poll.status == PollStatus.active, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return controller.twitchEventSub?.currentPoll.value != null + ? ValueListenableBuilder( + valueListenable: controller.twitchEventSub!.currentPoll, + builder: (context, poll, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Poll", + style: TextStyle( + color: Theme.of(Get.context!).textTheme.bodyLarge!.color, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + poll != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextButton( - style: TextButton.styleFrom( - textStyle: const TextStyle(fontSize: 12), - backgroundColor: Theme.of(context) - .colorScheme - .tertiaryContainer, - ), - onPressed: () { - controller.endPoll("ARCHIVED"); + Text(poll.title), + const SizedBox(height: 10), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: poll.choices.length, + itemBuilder: (context, index) { + final choice = poll.choices[index]; + final percentage = poll.totalVotes > 0 + ? choice.votes / poll.totalVotes + : 0.0; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + choice.title, + style: TextStyle( + color: Theme.of(Get.context!) + .textTheme + .bodyLarge! + .color, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 3), + LinearPercentIndicator( + animation: true, + animateFromLastPercent: true, + barRadius: const Radius.circular(8), + padding: const EdgeInsets.symmetric( + horizontal: 0.0), + lineHeight: 20.0, + percent: percentage, + backgroundColor: + Theme.of(context).colorScheme.secondary, + progressColor: ((poll.status == + PollStatus.completed) && + percentage > 0.5) + ? Colors.green + : Theme.of(context) + .colorScheme + .tertiaryContainer, + center: Text( + "${(percentage * 100).toStringAsFixed(2)} %"), + ), + const SizedBox(height: 10), + ], + ); }, - child: Text( - "cancel".tr, - style: const TextStyle( - color: Colors.white, - ), - ), ), - TextButton( - style: TextButton.styleFrom( - textStyle: const TextStyle(fontSize: 12), - backgroundColor: Colors.green, - ), - onPressed: () { - controller.endPoll("TERMINATED"); - }, - child: const Text( - 'End', - style: TextStyle( - color: Colors.white, - ), + Visibility( + visible: poll.status == PollStatus.active, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 12), + backgroundColor: Theme.of(context) + .colorScheme + .tertiaryContainer, + ), + onPressed: () { + controller.endPoll("ARCHIVED"); + }, + child: Text( + "cancel".tr, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + TextButton( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 12), + backgroundColor: Colors.green, + ), + onPressed: () { + controller.endPoll("TERMINATED"); + }, + child: const Text( + 'End', + style: TextStyle( + color: Colors.white, + ), + ), + ), + ], ), ), ], - ), - ), - ], - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "No poll running", - style: TextStyle( - color: Theme.of(context).textTheme.bodyLarge!.color, - ), - ), - TextButton( - style: TextButton.styleFrom( - textStyle: const TextStyle(fontSize: 12), - backgroundColor: Colors.deepPurpleAccent, - ), - onPressed: () {}, - child: const Text( - 'Create one', - style: TextStyle( - color: Colors.white, - ), - ), - ), - ], + ) + : Container(), + const SizedBox( + height: 10, ), - const SizedBox( - height: 10, - ), - ], - ); - }, - ); + ], + ); + }, + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "No poll running", + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + ), + ), + // TextButton( + // style: TextButton.styleFrom( + // textStyle: const TextStyle(fontSize: 12), + // backgroundColor: Colors.deepPurpleAccent, + // ), + // onPressed: () {}, + // child: const Text( + // 'Create one', + // style: TextStyle( + // color: Colors.white, + // ), + // ), + // ), + ], + ); } Widget _shortcutButton({ @@ -631,4 +651,4 @@ void pickWinnerDialog(BuildContext context, TwitchPrediction prediction, ), ), ); -} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 4e6325bb..9deba6a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" connectivity_plus: dependency: "direct main" description: @@ -636,18 +636,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -921,10 +921,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" split_view: dependency: "direct main" description: @@ -977,10 +977,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" timezone: dependency: transitive description: @@ -1191,6 +1191,14 @@ packages: url: "https://github.com/chandrabezzo/wakelock.git" source: git version: "0.2.2" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: "direct main" description: @@ -1272,5 +1280,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.1 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index a88ecee7..7673f708 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.7.0+28 +version: 1.7.0+29 environment: sdk: '>=2.19.0-0 <4.0.0'