feat: setup intl

closes #29
This commit is contained in:
2025-05-20 15:48:51 +01:00
parent 182f34ff71
commit a0b2275bea
24 changed files with 955 additions and 270 deletions

View File

@ -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';

View File

@ -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!),
),

View File

@ -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),
),
],

View File

@ -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),
),
],

View File

@ -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,
),
),
],
),

View File

@ -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

View File

@ -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,
);
},
);
}
}

View File

@ -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()}",
),
],
),
);
},
),
);
}

View File

@ -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,
),
],
),

View File

@ -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),
),
),
),
],

View 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,
);
},
);
}
}

View File

@ -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,

View File

@ -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),
],
),
),
);

View File

@ -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,

View File

@ -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),
),
});
}),
],
),
);
},

View File

@ -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()),
],
);
},

View File

@ -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(

View File

@ -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();