mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-16 20:08:50 +00:00
@ -1,4 +1,3 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
|
||||
@ -90,7 +91,7 @@ class _AvatarUpload extends State<AvatarUpload> {
|
||||
child:
|
||||
_loading
|
||||
? CircularProgressIndicator()
|
||||
: Text("Upload Avatar"),
|
||||
: Text(t.upload_avatar),
|
||||
)
|
||||
: CachedNetworkImage(imageUrl: _avatar!),
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/widgets/button.dart';
|
||||
@ -49,7 +50,9 @@ class FollowButton extends StatelessWidget {
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
isFollowing ? "Unfollow" : "Follow",
|
||||
isFollowing
|
||||
? t.button.unfollow
|
||||
: t.button.follow,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/rx_filter.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
@ -28,7 +29,7 @@ class CategoryTopZapped extends StatelessWidget {
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
),
|
||||
TextSpan(
|
||||
text: " Most Zapped Streamers",
|
||||
text: " ${t.most_zapped_streamers}",
|
||||
style: TextStyle(color: LAYER_4, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/rx_filter.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
@ -11,6 +12,7 @@ import 'package:zap_stream_flutter/widgets/chat_raid.dart';
|
||||
import 'package:zap_stream_flutter/widgets/chat_timeout.dart';
|
||||
import 'package:zap_stream_flutter/widgets/chat_write.dart';
|
||||
import 'package:zap_stream_flutter/widgets/chat_zap.dart';
|
||||
import 'package:zap_stream_flutter/widgets/countdown.dart';
|
||||
import 'package:zap_stream_flutter/widgets/goal.dart';
|
||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||
|
||||
@ -75,7 +77,9 @@ class ChatWidget extends StatelessWidget {
|
||||
9735 => ZapReceipt.fromEvent(e).sender ?? e.pubKey,
|
||||
_ => e.pubKey,
|
||||
};
|
||||
return moderators.contains(author) || // cant mute self or host
|
||||
return moderators.contains(
|
||||
author,
|
||||
) || // cant mute self or host
|
||||
!mutedPubkeys.contains(author);
|
||||
})
|
||||
// filter events that are created before stream start time
|
||||
@ -159,7 +163,7 @@ class ChatWidget extends StatelessWidget {
|
||||
color: PRIMARY_1,
|
||||
),
|
||||
child: Text(
|
||||
"STREAM ENDED",
|
||||
t.stream.chat.ended,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
@ -181,19 +185,18 @@ class ChatWidget extends StatelessWidget {
|
||||
decoration: BoxDecoration(color: WARNING),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("CHAT DISABLED", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
t.stream.chat.disabled,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (timeoutEvent != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("Timeout expires: "),
|
||||
CountdownTimer(
|
||||
onTrigger: () => {},
|
||||
triggerAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
int.parse(timeoutEvent.getFirstTag("expiration")!) * 1000,
|
||||
),
|
||||
),
|
||||
],
|
||||
CountdownTimer(
|
||||
onTrigger: () => {},
|
||||
format: (time) => t.stream.chat.disabled_timeout(time: time),
|
||||
style: TextStyle(color: LAYER_5),
|
||||
triggerAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
int.parse(timeoutEvent.getFirstTag("expiration")!) * 1000,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:ndk/entities.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
@ -60,7 +61,7 @@ class ChatBadgeAwardWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Awarded to: ",
|
||||
"${t.stream.chat.badge.awarded_to} ",
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
...event
|
||||
|
@ -1,11 +1,12 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:duration/duration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
import 'package:zap_stream_flutter/widgets/countdown.dart';
|
||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||
|
||||
class ChatRaidMessage extends StatefulWidget {
|
||||
@ -74,64 +75,57 @@ class __ChatRaidMessage extends State<ChatRaidMessage>
|
||||
final otherStreamEvent = StreamEvent(otherStream);
|
||||
return Column(
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
children: [
|
||||
TextSpan(text: _isRaiding ? "RAIDING " : "RAID FROM "),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: ProfileLoaderWidget(otherStreamEvent.info.host, (
|
||||
ctx,
|
||||
profile,
|
||||
) {
|
||||
return Text(
|
||||
ProfileNameWidget.nameFromProfile(
|
||||
profile.data ??
|
||||
Metadata(pubKey: otherStreamEvent.info.host),
|
||||
).toUpperCase(),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
}),
|
||||
),
|
||||
if (_raidingAt == null)
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
context.go(
|
||||
"/e/${otherStreamEvent.link}",
|
||||
extra: otherStreamEvent,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(Icons.open_in_new, size: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ProfileLoaderWidget(otherStreamEvent.info.host, (
|
||||
ctx,
|
||||
profile,
|
||||
) {
|
||||
final otherMeta =
|
||||
profile.data ??
|
||||
Metadata(pubKey: otherStreamEvent.info.host);
|
||||
return Text(
|
||||
_isRaiding
|
||||
? t.stream.chat.raid.to(
|
||||
name:
|
||||
ProfileNameWidget.nameFromProfile(
|
||||
otherMeta,
|
||||
).toUpperCase(),
|
||||
)
|
||||
: t.stream.chat.raid.from(
|
||||
name:
|
||||
ProfileNameWidget.nameFromProfile(
|
||||
otherMeta,
|
||||
).toUpperCase(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
}),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.go(
|
||||
"/e/${otherStreamEvent.link}",
|
||||
extra: otherStreamEvent,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(Icons.open_in_new, size: 15),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_raidingAt != null)
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: "Raiding in "),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CountdownTimer(
|
||||
triggerAt: _raidingAt!,
|
||||
onTrigger: () {
|
||||
context.go(
|
||||
"/e/${otherStreamEvent.link}",
|
||||
extra: otherStreamEvent,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
CountdownTimer(
|
||||
format: (time) => t.stream.chat.raid.countdown(time: time),
|
||||
triggerAt: _raidingAt!,
|
||||
onTrigger: () {
|
||||
context.go(
|
||||
"/e/${otherStreamEvent.link}",
|
||||
extra: otherStreamEvent,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -140,74 +134,3 @@ class __ChatRaidMessage extends State<ChatRaidMessage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CountdownTimer extends StatefulWidget {
|
||||
final void Function() onTrigger;
|
||||
final TextStyle? style;
|
||||
final DateTime triggerAt;
|
||||
|
||||
const CountdownTimer({
|
||||
super.key,
|
||||
required this.onTrigger,
|
||||
this.style,
|
||||
required this.triggerAt,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _CountdownTimerState();
|
||||
}
|
||||
|
||||
class _CountdownTimerState extends State<CountdownTimer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
bool _actionTriggered = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
final countdown =
|
||||
widget.triggerAt.isBefore(now)
|
||||
? Duration()
|
||||
: widget.triggerAt.difference(now);
|
||||
|
||||
_controller = AnimationController(vsync: this, duration: countdown);
|
||||
|
||||
// Create animation to track progress from 5 to 0
|
||||
_animation = Tween<double>(
|
||||
begin: countdown.inSeconds.toDouble(),
|
||||
end: 0,
|
||||
).animate(_controller)..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed && !_actionTriggered) {
|
||||
setState(() {
|
||||
_actionTriggered = true;
|
||||
widget.onTrigger();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start the countdown immediately when widget is mounted
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose(); // Clean up the controller
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
final secondsLeft = _animation.value.ceil();
|
||||
return Text(
|
||||
Duration(seconds: secondsLeft).pretty(abbreviated: true),
|
||||
style: widget.style,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:duration/duration.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||
|
||||
@ -11,32 +14,43 @@ class ChatTimeoutWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pTags = timeout.pTags;
|
||||
final duration =
|
||||
double.parse(timeout.getFirstTag("expiration")!) - timeout.createdAt;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(color: LAYER_5),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: ProfileNameWidget.pubkey(timeout.pubKey),
|
||||
),
|
||||
TextSpan(text: " timed out "),
|
||||
...pTags.map(
|
||||
(p) => WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: ProfileNameWidget.pubkey(p),
|
||||
child: FutureBuilder(
|
||||
future: ndk.metadata.loadMetadatas([
|
||||
timeout.pubKey,
|
||||
...timeout.pTags,
|
||||
], null),
|
||||
builder: (context, state) {
|
||||
final modProfile =
|
||||
state.data?.firstWhereOrNull((p) => p.pubKey == timeout.pubKey) ??
|
||||
Metadata(pubKey: timeout.pubKey);
|
||||
final userProfiles = timeout.pTags.map(
|
||||
(p) =>
|
||||
state.data?.firstWhereOrNull((x) => x.pubKey == p) ??
|
||||
Metadata(pubKey: p),
|
||||
);
|
||||
|
||||
return Text.rich(
|
||||
style: TextStyle(color: LAYER_5),
|
||||
t.stream.chat.timeout(
|
||||
mod: TextSpan(
|
||||
text: ProfileNameWidget.nameFromProfile(modProfile),
|
||||
),
|
||||
user: TextSpan(
|
||||
text: userProfiles
|
||||
.map((p) => ProfileNameWidget.nameFromProfile(p))
|
||||
.join(", "),
|
||||
),
|
||||
time: TextSpan(
|
||||
text: Duration(seconds: duration.floor()).pretty(),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " for ${Duration(seconds: duration.toInt()).pretty()}",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
@ -90,7 +91,7 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
future: ndkCache.searchMetadatas(search, 5),
|
||||
builder: (context, state) {
|
||||
if (state.data?.isEmpty ?? true) {
|
||||
return Text("No user found");
|
||||
return Text(t.no_user_found);
|
||||
}
|
||||
|
||||
return Column(
|
||||
@ -223,7 +224,7 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
controller: _controller,
|
||||
onSubmitted: (_) => _sendMessage(context),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Write message",
|
||||
labelText: t.stream.chat.write.label,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 4),
|
||||
labelStyle: TextStyle(color: LAYER_4, fontSize: 14),
|
||||
border: InputBorder.none,
|
||||
@ -255,8 +256,8 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
||||
children: [
|
||||
Text(
|
||||
isLogin
|
||||
? "Can't write messages with npub login"
|
||||
: "Please login to send messages",
|
||||
? t.stream.chat.write.no_signer
|
||||
: t.stream.chat.write.login,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
import 'package:zap_stream_flutter/widgets/avatar.dart';
|
||||
@ -24,14 +25,14 @@ class ChatZapWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_zapperRowZap(parsed),
|
||||
_zapperRowZap(context, parsed),
|
||||
if (parsed.comment?.isNotEmpty ?? false) Text(parsed.comment!),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _zapperRowZap(ZapReceipt parsed) {
|
||||
Widget _zapperRowZap(BuildContext context, ZapReceipt parsed) {
|
||||
if (parsed.sender != null) {
|
||||
return ProfileLoaderWidget(parsed.sender!, (ctx, state) {
|
||||
final name = ProfileNameWidget.nameFromProfile(
|
||||
@ -40,35 +41,23 @@ class ChatZapWidget extends StatelessWidget {
|
||||
return _zapperRow(name, parsed.amountSats ?? 0, state.data);
|
||||
});
|
||||
} else {
|
||||
return _zapperRow("Anon", parsed.amountSats ?? 0, null);
|
||||
return _zapperRow(t.anon, parsed.amountSats ?? 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _zapperRow(String name, int amount, Metadata? profile) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (profile != null) AvatarWidget(profile: profile, size: 24),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(color: ZAP_1),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Icon(Icons.bolt, color: ZAP_1),
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
),
|
||||
if (profile != null)
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: AvatarWidget(profile: profile, size: 20),
|
||||
),
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
),
|
||||
TextSpan(text: name),
|
||||
TextSpan(text: " zapped ", style: TextStyle(color: FONT_COLOR)),
|
||||
TextSpan(text: formatSats(amount)),
|
||||
TextSpan(text: " sats", style: TextStyle(color: FONT_COLOR)),
|
||||
],
|
||||
text: t.stream.chat.zap(
|
||||
user: TextSpan(text: name, style: TextStyle(color: ZAP_1)),
|
||||
amount: TextSpan(
|
||||
text: formatSats(amount),
|
||||
style: TextStyle(color: ZAP_1),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
76
lib/widgets/countdown.dart
Normal file
76
lib/widgets/countdown.dart
Normal file
@ -0,0 +1,76 @@
|
||||
import 'package:duration/duration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CountdownTimer extends StatefulWidget {
|
||||
final void Function() onTrigger;
|
||||
final TextStyle? style;
|
||||
final DateTime triggerAt;
|
||||
final String Function(String time)? format;
|
||||
|
||||
const CountdownTimer({
|
||||
super.key,
|
||||
required this.onTrigger,
|
||||
this.style,
|
||||
required this.triggerAt,
|
||||
this.format,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _CountdownTimerState();
|
||||
}
|
||||
|
||||
class _CountdownTimerState extends State<CountdownTimer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
bool _actionTriggered = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
final countdown =
|
||||
widget.triggerAt.isBefore(now)
|
||||
? Duration()
|
||||
: widget.triggerAt.difference(now);
|
||||
|
||||
_controller = AnimationController(vsync: this, duration: countdown);
|
||||
|
||||
// Create animation to track progress from 5 to 0
|
||||
_animation = Tween<double>(
|
||||
begin: countdown.inSeconds.toDouble(),
|
||||
end: 0,
|
||||
).animate(_controller)..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed && !_actionTriggered) {
|
||||
setState(() {
|
||||
_actionTriggered = true;
|
||||
widget.onTrigger();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start the countdown immediately when widget is mounted
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose(); // Clean up the controller
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
final secondsLeft = _animation.value.ceil();
|
||||
final v = Duration(seconds: secondsLeft).pretty(abbreviated: true);
|
||||
return Text(
|
||||
widget.format != null ? widget.format!(v) : v,
|
||||
style: widget.style,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/rx_filter.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
@ -50,7 +51,7 @@ class GoalWidget extends StatelessWidget {
|
||||
Expanded(child: Text(goal.content)),
|
||||
if (remaining > 0)
|
||||
Text(
|
||||
"Remaining: ${formatSats(remaining)}",
|
||||
t.goal.remaining(amount: formatSats(remaining)),
|
||||
style: TextStyle(fontSize: 10, color: LAYER_5),
|
||||
),
|
||||
],
|
||||
@ -76,7 +77,7 @@ class GoalWidget extends StatelessWidget {
|
||||
Positioned(
|
||||
right: 2,
|
||||
child: Text(
|
||||
"Goal: ${formatSats((max / 1000).toInt())}",
|
||||
t.goal.title(amount: formatSats((max / 1000).floor())),
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -86,7 +87,7 @@ class GoalWidget extends StatelessWidget {
|
||||
if (remaining == 0)
|
||||
Center(
|
||||
child: Text(
|
||||
"COMPLETE",
|
||||
t.goal.complete,
|
||||
style: TextStyle(
|
||||
color: LAYER_0,
|
||||
fontSize: 8,
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/widgets/avatar.dart';
|
||||
@ -58,7 +59,10 @@ class LoginButtonWidget extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [Text("Login"), Icon(Icons.login, size: 16)],
|
||||
children: [
|
||||
Text(t.button.login),
|
||||
Icon(Icons.login, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:ndk/domain_layer/entities/nip_51_list.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/widgets/button.dart';
|
||||
@ -32,7 +33,7 @@ class MuteButton extends StatelessWidget {
|
||||
final isMuted = mutes.contains(pubkey);
|
||||
return BasicButton(
|
||||
Text(
|
||||
isMuted ? "Unmute" : "Mute",
|
||||
isMuted ? t.button.unmute : t.button.mute,
|
||||
style: TextStyle(
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/rx_filter.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
@ -22,44 +23,45 @@ class NoteEmbedWidget extends StatelessWidget {
|
||||
filters: [entity.toFilter()],
|
||||
builder: (context, data) {
|
||||
final note = data != null && data.isNotEmpty ? data.first : null;
|
||||
if (note == null) return SizedBox.shrink();
|
||||
|
||||
final author = switch (note.kind) {
|
||||
30_311 => StreamEvent(note).info.host,
|
||||
_ => note.pubKey,
|
||||
};
|
||||
return PillWidget(
|
||||
onTap: () {
|
||||
if (note != null) {
|
||||
// redirect to the stream if its a live stream link
|
||||
if (note.kind == 30_311) {
|
||||
context.push("/e/$link", extra: StreamEvent(note));
|
||||
return;
|
||||
}
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SingleChildScrollView(child: _NotePreview(note: note));
|
||||
},
|
||||
);
|
||||
// redirect to the stream if its a live stream link
|
||||
if (note.kind == 30_311) {
|
||||
context.push("/e/$link", extra: StreamEvent(note));
|
||||
return;
|
||||
}
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SingleChildScrollView(child: _NotePreview(note: note));
|
||||
},
|
||||
);
|
||||
},
|
||||
color: LAYER_3,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
WidgetSpan(child: Icon(Icons.link, size: 16)),
|
||||
TextSpan(
|
||||
text: switch (entity.kind) {
|
||||
30_023 => " Article by ",
|
||||
30_311 => " Live Stream by ",
|
||||
_ => " Note by ",
|
||||
},
|
||||
),
|
||||
if (note?.pubKey != null)
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: ProfileNameWidget.pubkey(switch (note!.kind) {
|
||||
30_311 => StreamEvent(note).info.host,
|
||||
_ => note.pubKey,
|
||||
}, linkToProfile: false),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.link, size: 16),
|
||||
ProfileLoaderWidget(author, (context, state) {
|
||||
final profile = state.data ?? Metadata(pubKey: note.pubKey);
|
||||
return Text(switch (entity.kind) {
|
||||
30_023 => t.embed.article_by(
|
||||
name: ProfileNameWidget.nameFromProfile(profile),
|
||||
),
|
||||
],
|
||||
),
|
||||
30_311 => t.embed.live_stream_by(
|
||||
name: ProfileNameWidget.nameFromProfile(profile),
|
||||
),
|
||||
_ => t.embed.note_by(
|
||||
name: ProfileNameWidget.nameFromProfile(profile),
|
||||
),
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
@ -54,13 +55,21 @@ class StreamGrid extends StatelessWidget {
|
||||
spacing: 16,
|
||||
children: [
|
||||
if (followsLive.isNotEmpty)
|
||||
_streamGroup(context, "Following", followsLive.toList()),
|
||||
_streamGroup(
|
||||
context,
|
||||
t.stream_list.following,
|
||||
followsLive.toList(),
|
||||
),
|
||||
if (showLive && liveNotFollowing.isNotEmpty)
|
||||
_streamGroup(context, "Live", liveNotFollowing.toList()),
|
||||
_streamGroup(
|
||||
context,
|
||||
t.stream_list.live,
|
||||
liveNotFollowing.toList(),
|
||||
),
|
||||
if (showPlanned && planned.isNotEmpty)
|
||||
_streamGroup(context, "Planned", planned.toList()),
|
||||
_streamGroup(context, t.stream_list.planned, planned.toList()),
|
||||
if (showEnded && ended.isNotEmpty)
|
||||
_streamGroup(context, "Ended", ended.toList()),
|
||||
_streamGroup(context, t.stream_list.ended, ended.toList()),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
@ -45,7 +46,7 @@ class StreamInfoWidget extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
BasicButton.text(
|
||||
"Share",
|
||||
t.button.share,
|
||||
icon: Icon(Icons.share, size: 16),
|
||||
onTap: () {
|
||||
SharePlus.instance.share(
|
||||
|
@ -8,6 +8,7 @@ import 'package:ndk/domain_layer/usecases/lnurl/lnurl.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
import 'package:zap_stream_flutter/theme.dart';
|
||||
import 'package:zap_stream_flutter/utils.dart';
|
||||
@ -59,20 +60,13 @@ class _ZapWidget extends State<ZapWidget> {
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 5,
|
||||
children: [
|
||||
Text(
|
||||
"Zap",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
ProfileNameWidget.pubkey(
|
||||
widget.pubkey,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
ProfileLoaderWidget(widget.pubkey, (context, state) {
|
||||
final profile = state.data ?? Metadata(pubKey: widget.pubkey);
|
||||
return Text(
|
||||
t.zap.title(name: ProfileNameWidget.nameFromProfile(profile)),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
);
|
||||
}),
|
||||
if (_pr == null && !_loading) ..._inputs(),
|
||||
if (_pr != null) ..._invoice(context),
|
||||
if (_loading) CircularProgressIndicator(),
|
||||
@ -102,11 +96,11 @@ class _ZapWidget extends State<ZapWidget> {
|
||||
controller: _customAmount,
|
||||
focusNode: _customAmountFocus,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(labelText: "Custom Amount"),
|
||||
decoration: InputDecoration(labelText: t.zap.custom_amount),
|
||||
),
|
||||
),
|
||||
BasicButton.text(
|
||||
"Confirm",
|
||||
t.zap.confirm,
|
||||
onTap: () {
|
||||
final newAmount = int.tryParse(_customAmount.text);
|
||||
if (newAmount != null) {
|
||||
@ -117,7 +111,7 @@ class _ZapWidget extends State<ZapWidget> {
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_error = "Invalid custom amount";
|
||||
_error = t.zap.error.invalid_custom_amount;
|
||||
_amount = null;
|
||||
});
|
||||
}
|
||||
@ -127,10 +121,12 @@ class _ZapWidget extends State<ZapWidget> {
|
||||
),
|
||||
TextFormField(
|
||||
controller: _comment,
|
||||
decoration: InputDecoration(labelText: "Comment"),
|
||||
decoration: InputDecoration(labelText: t.zap.comment),
|
||||
),
|
||||
BasicButton.text(
|
||||
_amount != null ? "Zap ${formatSats(_amount!)} sats" : "Zap",
|
||||
_amount != null
|
||||
? t.zap.button_zap_ready(amount: formatSats(_amount!))
|
||||
: t.zap.button_zap,
|
||||
disabled: _amount == null,
|
||||
decoration: BoxDecoration(color: LAYER_3, borderRadius: DEFAULT_BR),
|
||||
onTap: () async {
|
||||
@ -179,7 +175,7 @@ class _ZapWidget extends State<ZapWidget> {
|
||||
if (Platform.isIOS && context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text("Copied to clipboard")));
|
||||
).showSnackBar(SnackBar(content: Text(t.zap.copy)));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
@ -195,7 +191,7 @@ class _ZapWidget extends State<ZapWidget> {
|
||||
),
|
||||
),
|
||||
BasicButton.text(
|
||||
"Open in Wallet",
|
||||
t.zap.button_open_wallet,
|
||||
onTap: () async {
|
||||
try {
|
||||
await launchUrlString(prLink);
|
||||
@ -203,7 +199,7 @@ class _ZapWidget extends State<ZapWidget> {
|
||||
if (e is PlatformException) {
|
||||
if (e.code == "ACTIVITY_NOT_FOUND") {
|
||||
setState(() {
|
||||
_error = "No lightning wallet installed";
|
||||
_error = t.zap.error.no_wallet;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -266,7 +262,7 @@ class _ZapWidget extends State<ZapWidget> {
|
||||
Future<void> _loadZap() async {
|
||||
final profile = await ndk.metadata.loadMetadata(widget.pubkey);
|
||||
if (profile?.lud16 == null) {
|
||||
throw "No lightning address found";
|
||||
throw t.zap.error.no_lud16;
|
||||
}
|
||||
|
||||
final zapRequest = await _makeZap();
|
||||
|
Reference in New Issue
Block a user