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

118
lib/i18n/en.i18n.json Normal file
View File

@ -0,0 +1,118 @@
{
"upload_avatar": "Upload Avatar",
"@upload_avatar": {
"description": "Text prompting user to hit avatar placeholder to begin upload"
},
"most_zapped_streamers": "Most Zapped Streamers",
"@most_zapped_streamers": {
"description": "Heading over listed top streamers by zaps"
},
"no_user_found": "No user found",
"@no_user_found": {
"description": "No user found when searching"
},
"anon": "Anon",
"@anon": {
"description": "An anonymous user"
},
"stream": {
"chat": {
"disabled": "CHAT DISABLED",
"disabled_timeout": "Timeout expires: $time",
"timeout(rich)": "$mod timed out $user for $time",
"@timeout": {
"description": "Chat message showing timeout events"
},
"ended": "STREAM ENDED",
"@ended": {
"description": "Stream ended footer at bottom of chat"
},
"zap(rich)": "$user zapped $amount sats",
"@zap": {
"description": "Chat message showing stream zaps"
},
"write": {
"label": "Write message",
"@label": {
"description": "Label on the chat message input box"
},
"no_signer": "Can't write messages with npub login",
"@no_signer": {
"description": "Chat input message shown when the user is logged in only with pubkey"
},
"login": "Please login to send messages",
"@login": {
"description": "Chat input message shown when the user is logged out"
}
},
"badge": {
"awarded_to": "Awarded to:",
"@awarded_to": {
"description": "Heading over list of users who are awarded a badge"
}
},
"raid": {
"to": "RAIDING $name",
"@to": {
"description": "Chat raid message to another stream"
},
"from": "RAID FROM $name",
"@from": {
"description": "Chat raid message from another stream"
},
"countdown": "Raiding in $time",
"@countdown": {
"description": "Countdown timer for auto-raiding"
}
}
}
},
"goal": {
"title": "Goal: $amount",
"remaining": "Remaining: $amount",
"complete": "COMPLETE"
},
"button": {
"login": "Login",
"@login": {
"description": "Button text for the login button"
},
"follow": "Follow",
"@follow": {
"description": "Button text for the follow button"
},
"unfollow": "Unfollow",
"@unfollow": {
"description": "Button text for the unfollow button"
},
"mute": "Mute",
"unmute": "Unmute",
"share": "Share"
},
"embed": {
"article_by": "Article by $name",
"note_by": "Note by $name",
"live_stream_by": "Live stream by $name"
},
"stream_list": {
"following": "Following",
"live": "Live",
"planned": "Planned",
"ended": "Ended"
},
"zap": {
"title": "Zap $name",
"custom_amount": "Custom Amount",
"confirm": "Confirm",
"comment": "Comment",
"button_zap_ready": "Zap $amount sats",
"button_zap": "Zap",
"button_open_wallet": "Open in Wallet",
"copy": "Copied to clipboard",
"error": {
"invalid_custom_amount": "Invalid custom amount",
"no_wallet": "No lightning wallet installed",
"no_lud16": "No lightning address found"
}
}
}

168
lib/i18n/strings.g.dart Normal file
View File

@ -0,0 +1,168 @@
/// Generated file. Do not edit.
///
/// Source: lib/i18n
/// To regenerate, run: `dart run slang`
///
/// Locales: 1
/// Strings: 43
///
/// Built on 2025-05-20 at 14:43 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:slang/generated.dart';
import 'package:slang_flutter/slang_flutter.dart';
export 'package:slang_flutter/slang_flutter.dart';
part 'strings_en.g.dart';
/// Supported locales.
///
/// Usage:
/// - LocaleSettings.setLocale(AppLocale.en) // set locale
/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
enum AppLocale with BaseAppLocale<AppLocale, Translations> {
en(languageCode: 'en');
const AppLocale({
required this.languageCode,
this.scriptCode, // ignore: unused_element, unused_element_parameter
this.countryCode, // ignore: unused_element, unused_element_parameter
});
@override final String languageCode;
@override final String? scriptCode;
@override final String? countryCode;
@override
Future<Translations> build({
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
}) async {
switch (this) {
case AppLocale.en:
return TranslationsEn(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
}
@override
Translations buildSync({
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
}) {
switch (this) {
case AppLocale.en:
return TranslationsEn(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
}
/// Gets current instance managed by [LocaleSettings].
Translations get translations => LocaleSettings.instance.getTranslations(this);
}
/// Method A: Simple
///
/// No rebuild after locale change.
/// Translation happens during initialization of the widget (call of t).
/// Configurable via 'translate_var'.
///
/// Usage:
/// String a = t.someKey.anotherKey;
/// String b = t['someKey.anotherKey']; // Only for edge cases!
Translations get t => LocaleSettings.instance.currentTranslations;
/// Method B: Advanced
///
/// All widgets using this method will trigger a rebuild when locale changes.
/// Use this if you have e.g. a settings page where the user can select the locale during runtime.
///
/// Step 1:
/// wrap your App with
/// TranslationProvider(
/// child: MyApp()
/// );
///
/// Step 2:
/// final t = Translations.of(context); // Get t variable.
/// String a = t.someKey.anotherKey; // Use t variable.
/// String b = t['someKey.anotherKey']; // Only for edge cases!
class TranslationProvider extends BaseTranslationProvider<AppLocale, Translations> {
TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance);
static InheritedLocaleData<AppLocale, Translations> of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(context);
}
/// Method B shorthand via [BuildContext] extension method.
/// Configurable via 'translate_var'.
///
/// Usage (e.g. in a widget's build method):
/// context.t.someKey.anotherKey
extension BuildContextTranslationsExtension on BuildContext {
Translations get t => TranslationProvider.of(this).translations;
}
/// Manages all translation instances and the current locale
class LocaleSettings extends BaseFlutterLocaleSettings<AppLocale, Translations> {
LocaleSettings._() : super(
utils: AppLocaleUtils.instance,
lazy: true,
);
static final instance = LocaleSettings._();
// static aliases (checkout base methods for documentation)
static AppLocale get currentLocale => instance.currentLocale;
static Stream<AppLocale> getLocaleStream() => instance.getLocaleStream();
static Future<AppLocale> setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);
static Future<AppLocale> setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static Future<AppLocale> useDeviceLocale() => instance.useDeviceLocale();
static Future<void> setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
// synchronous versions
static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync();
static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
/// Provides utility functions without any side effects.
class AppLocaleUtils extends BaseAppLocaleUtils<AppLocale, Translations> {
AppLocaleUtils._() : super(
baseLocale: AppLocale.en,
locales: AppLocale.values,
);
static final instance = AppLocaleUtils._();
// static aliases (checkout base methods for documentation)
static AppLocale parse(String rawLocale) => instance.parse(rawLocale);
static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode);
static AppLocale findDeviceLocale() => instance.findDeviceLocale();
static List<Locale> get supportedLocales => instance.supportedLocales;
static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
}

308
lib/i18n/strings_en.g.dart Normal file
View File

@ -0,0 +1,308 @@
///
/// Generated file. Do not edit.
///
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
part of 'strings.g.dart';
// Path: <root>
typedef TranslationsEn = Translations; // ignore: unused_element
class Translations implements BaseTranslations<AppLocale, Translations> {
/// Returns the current translations of the given [context].
///
/// Usage:
/// final t = Translations.of(context);
static Translations of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(context).translations;
/// You can call this constructor and build your own translation instance of this locale.
/// Constructing via the enum [AppLocale.build] is preferred.
Translations({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata<AppLocale, Translations>? meta})
: assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'),
$meta = meta ?? TranslationMetadata(
locale: AppLocale.en,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
) {
$meta.setFlatMapFunction(_flatMapFunction);
}
/// Metadata for the translations of <en>.
@override final TranslationMetadata<AppLocale, Translations> $meta;
/// Access flat map
dynamic operator[](String key) => $meta.getTranslation(key);
late final Translations _root = this; // ignore: unused_field
Translations $copyWith({TranslationMetadata<AppLocale, Translations>? meta}) => Translations(meta: meta ?? this.$meta);
// Translations
/// Text prompting user to hit avatar placeholder to begin upload
String get upload_avatar => 'Upload Avatar';
/// Heading over listed top streamers by zaps
String get most_zapped_streamers => 'Most Zapped Streamers';
/// No user found when searching
String get no_user_found => 'No user found';
/// An anonymous user
String get anon => 'Anon';
late final TranslationsStreamEn stream = TranslationsStreamEn._(_root);
late final TranslationsGoalEn goal = TranslationsGoalEn._(_root);
late final TranslationsButtonEn button = TranslationsButtonEn._(_root);
late final TranslationsEmbedEn embed = TranslationsEmbedEn._(_root);
late final TranslationsStreamListEn stream_list = TranslationsStreamListEn._(_root);
late final TranslationsZapEn zap = TranslationsZapEn._(_root);
}
// Path: stream
class TranslationsStreamEn {
TranslationsStreamEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
late final TranslationsStreamChatEn chat = TranslationsStreamChatEn._(_root);
}
// Path: goal
class TranslationsGoalEn {
TranslationsGoalEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
String title({ required Object amount}) => 'Goal: ${amount}';
String remaining({ required Object amount}) => 'Remaining: ${amount}';
String get complete => 'COMPLETE';
}
// Path: button
class TranslationsButtonEn {
TranslationsButtonEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// Button text for the login button
String get login => 'Login';
/// Button text for the follow button
String get follow => 'Follow';
/// Button text for the unfollow button
String get unfollow => 'Unfollow';
String get mute => 'Mute';
String get unmute => 'Unmute';
String get share => 'Share';
}
// Path: embed
class TranslationsEmbedEn {
TranslationsEmbedEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
String article_by({ required Object name}) => 'Article by ${name}';
String note_by({ required Object name}) => 'Note by ${name}';
String live_stream_by({ required Object name}) => 'Live stream by ${name}';
}
// Path: stream_list
class TranslationsStreamListEn {
TranslationsStreamListEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get following => 'Following';
String get live => 'Live';
String get planned => 'Planned';
String get ended => 'Ended';
}
// Path: zap
class TranslationsZapEn {
TranslationsZapEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
String title({ required Object name}) => 'Zap ${name}';
String get custom_amount => 'Custom Amount';
String get confirm => 'Confirm';
String get comment => 'Comment';
String button_zap_ready({ required Object amount}) => 'Zap ${amount} sats';
String get button_zap => 'Zap';
String get button_open_wallet => 'Open in Wallet';
String get copy => 'Copied to clipboard';
late final TranslationsZapErrorEn error = TranslationsZapErrorEn._(_root);
}
// Path: stream.chat
class TranslationsStreamChatEn {
TranslationsStreamChatEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get disabled => 'CHAT DISABLED';
String disabled_timeout({ required Object time}) => 'Timeout expires: ${time}';
/// Chat message showing timeout events
TextSpan timeout({ required InlineSpan mod, required InlineSpan user, required InlineSpan time, TextStyle? style, GestureRecognizer? recognizer}) => TextSpan(children: [
mod,
const TextSpan(text: ' timed out '),
user,
const TextSpan(text: ' for '),
time,
], style: style, recognizer: recognizer);
/// Stream ended footer at bottom of chat
String get ended => 'STREAM ENDED';
/// Chat message showing stream zaps
TextSpan zap({ required InlineSpan user, required InlineSpan amount, TextStyle? style, GestureRecognizer? recognizer}) => TextSpan(children: [
user,
const TextSpan(text: ' zapped '),
amount,
const TextSpan(text: ' sats'),
], style: style, recognizer: recognizer);
late final TranslationsStreamChatWriteEn write = TranslationsStreamChatWriteEn._(_root);
late final TranslationsStreamChatBadgeEn badge = TranslationsStreamChatBadgeEn._(_root);
late final TranslationsStreamChatRaidEn raid = TranslationsStreamChatRaidEn._(_root);
}
// Path: zap.error
class TranslationsZapErrorEn {
TranslationsZapErrorEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get invalid_custom_amount => 'Invalid custom amount';
String get no_wallet => 'No lightning wallet installed';
String get no_lud16 => 'No lightning address found';
}
// Path: stream.chat.write
class TranslationsStreamChatWriteEn {
TranslationsStreamChatWriteEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// Label on the chat message input box
String get label => 'Write message';
/// Chat input message shown when the user is logged in only with pubkey
String get no_signer => 'Can\'t write messages with npub login';
/// Chat input message shown when the user is logged out
String get login => 'Please login to send messages';
}
// Path: stream.chat.badge
class TranslationsStreamChatBadgeEn {
TranslationsStreamChatBadgeEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// Heading over list of users who are awarded a badge
String get awarded_to => 'Awarded to:';
}
// Path: stream.chat.raid
class TranslationsStreamChatRaidEn {
TranslationsStreamChatRaidEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// Chat raid message to another stream
String to({ required Object name}) => 'RAIDING ${name}';
/// Chat raid message from another stream
String from({ required Object name}) => 'RAID FROM ${name}';
/// Countdown timer for auto-raiding
String countdown({ required Object time}) => 'Raiding in ${time}';
}
/// Flat map(s) containing all translations.
/// Only for edge cases! For simple maps, use the map function of this library.
extension on Translations {
dynamic _flatMapFunction(String path) {
switch (path) {
case 'upload_avatar': return 'Upload Avatar';
case 'most_zapped_streamers': return 'Most Zapped Streamers';
case 'no_user_found': return 'No user found';
case 'anon': return 'Anon';
case 'stream.chat.disabled': return 'CHAT DISABLED';
case 'stream.chat.disabled_timeout': return ({ required Object time}) => 'Timeout expires: ${time}';
case 'stream.chat.timeout': return ({ required InlineSpan mod, required InlineSpan user, required InlineSpan time, TextStyle? style, GestureRecognizer? recognizer}) => TextSpan(children: [
mod,
const TextSpan(text: ' timed out '),
user,
const TextSpan(text: ' for '),
time,
], style: style, recognizer: recognizer);
case 'stream.chat.ended': return 'STREAM ENDED';
case 'stream.chat.zap': return ({ required InlineSpan user, required InlineSpan amount, TextStyle? style, GestureRecognizer? recognizer}) => TextSpan(children: [
user,
const TextSpan(text: ' zapped '),
amount,
const TextSpan(text: ' sats'),
], style: style, recognizer: recognizer);
case 'stream.chat.write.label': return 'Write message';
case 'stream.chat.write.no_signer': return 'Can\'t write messages with npub login';
case 'stream.chat.write.login': return 'Please login to send messages';
case 'stream.chat.badge.awarded_to': return 'Awarded to:';
case 'stream.chat.raid.to': return ({ required Object name}) => 'RAIDING ${name}';
case 'stream.chat.raid.from': return ({ required Object name}) => 'RAID FROM ${name}';
case 'stream.chat.raid.countdown': return ({ required Object time}) => 'Raiding in ${time}';
case 'goal.title': return ({ required Object amount}) => 'Goal: ${amount}';
case 'goal.remaining': return ({ required Object amount}) => 'Remaining: ${amount}';
case 'goal.complete': return 'COMPLETE';
case 'button.login': return 'Login';
case 'button.follow': return 'Follow';
case 'button.unfollow': return 'Unfollow';
case 'button.mute': return 'Mute';
case 'button.unmute': return 'Unmute';
case 'button.share': return 'Share';
case 'embed.article_by': return ({ required Object name}) => 'Article by ${name}';
case 'embed.note_by': return ({ required Object name}) => 'Note by ${name}';
case 'embed.live_stream_by': return ({ required Object name}) => 'Live stream by ${name}';
case 'stream_list.following': return 'Following';
case 'stream_list.live': return 'Live';
case 'stream_list.planned': return 'Planned';
case 'stream_list.ended': return 'Ended';
case 'zap.title': return ({ required Object name}) => 'Zap ${name}';
case 'zap.custom_amount': return 'Custom Amount';
case 'zap.confirm': return 'Confirm';
case 'zap.comment': return 'Comment';
case 'zap.button_zap_ready': return ({ required Object amount}) => 'Zap ${amount} sats';
case 'zap.button_zap': return 'Zap';
case 'zap.button_open_wallet': return 'Open in Wallet';
case 'zap.copy': return 'Copied to clipboard';
case 'zap.error.invalid_custom_amount': return 'Invalid custom amount';
case 'zap.error.no_wallet': return 'No lightning wallet installed';
case 'zap.error.no_lud16': return 'No lightning address found';
default: return null;
}
}
}

View File

@ -1,11 +1,13 @@
import 'package:amberflutter/amberflutter.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:ndk/ndk.dart';
import 'package:ndk_amber/ndk_amber.dart';
import 'package:ndk_objectbox/ndk_objectbox.dart';
import 'package:ndk_rust_verifier/ndk_rust_verifier.dart';
import 'package:zap_stream_flutter/i18n/strings.g.dart';
import 'package:zap_stream_flutter/pages/category.dart';
import 'package:zap_stream_flutter/pages/hashtag.dart';
import 'package:zap_stream_flutter/pages/login.dart';
@ -55,6 +57,7 @@ final RouteObserver<ModalRoute<void>> routeObserver =
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
LocaleSettings.useDeviceLocale();
// reload / cache login data
loginData.addListener(() {
@ -89,6 +92,9 @@ Future<void> main() async {
runApp(
MaterialApp.router(
title: "zap.stream",
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
theme: ThemeData.localize(
ThemeData(colorScheme: ColorScheme.dark(), highlightColor: PRIMARY_1),
TextTheme(),
@ -104,7 +110,6 @@ Future<void> main() async {
routes: [
GoRoute(path: "/", builder: (ctx, state) => HomePage()),
ShellRoute(
observers: [routeObserver],
builder: (context, state, child) {
return Container(
margin: EdgeInsets.only(top: 50),

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,22 +185,21 @@ 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: () => {},
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,30 +75,34 @@ class __ChatRaidMessage extends State<ChatRaidMessage>
final otherStreamEvent = StreamEvent(otherStream);
return Column(
children: [
RichText(
text: TextSpan(
style: TextStyle(fontWeight: FontWeight.bold),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextSpan(text: _isRaiding ? "RAIDING " : "RAID FROM "),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: ProfileLoaderWidget(otherStreamEvent.info.host, (
ProfileLoaderWidget(otherStreamEvent.info.host, (
ctx,
profile,
) {
return Text(
ProfileNameWidget.nameFromProfile(
final otherMeta =
profile.data ??
Metadata(pubKey: otherStreamEvent.info.host),
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),
);
}),
),
if (_raidingAt == null)
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
GestureDetector(
onTap: () {
context.go(
"/e/${otherStreamEvent.link}",
@ -109,18 +114,11 @@ class __ChatRaidMessage extends State<ChatRaidMessage>
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(
CountdownTimer(
format: (time) => t.stream.chat.raid.countdown(time: time),
triggerAt: _raidingAt!,
onTrigger: () {
context.go(
@ -129,10 +127,6 @@ class __ChatRaidMessage extends State<ChatRaidMessage>
);
},
),
),
],
),
),
],
);
},
@ -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(
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),
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: ProfileNameWidget.pubkey(timeout.pubKey),
t.stream.chat.timeout(
mod: TextSpan(
text: ProfileNameWidget.nameFromProfile(modProfile),
),
TextSpan(text: " timed out "),
...pTags.map(
(p) => WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: ProfileNameWidget.pubkey(p),
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(
text: t.stream.chat.zap(
user: TextSpan(text: name, style: TextStyle(color: ZAP_1)),
amount: TextSpan(
text: formatSats(amount),
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)),
],
),
),
],

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,9 +23,14 @@ 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));
@ -36,31 +42,27 @@ class NoteEmbedWidget extends StatelessWidget {
return SingleChildScrollView(child: _NotePreview(note: note));
},
);
}
},
color: LAYER_3,
child: RichText(
text: TextSpan(
child: Row(
children: [
WidgetSpan(child: Icon(Icons.link, size: 16)),
TextSpan(
text: switch (entity.kind) {
30_023 => " Article by ",
30_311 => " Live Stream by ",
_ => " Note by ",
},
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),
),
if (note?.pubKey != null)
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: ProfileNameWidget.pubkey(switch (note!.kind) {
30_311 => StreamEvent(note).info.host,
_ => note.pubKey,
}, linkToProfile: false),
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",
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),
),
ProfileNameWidget.pubkey(
widget.pubkey,
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();

View File

@ -161,6 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
csv:
dependency: transitive
description:
name: csv
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
url: "https://pub.dev"
source: hosted
version: "6.0.0"
cupertino_icons:
dependency: transitive
description:
@ -302,6 +310,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_markdown_plus:
dependency: "direct main"
description:
@ -396,10 +409,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "2b9ba6d4c247457c35a6622f1dee6aab6694a4e15237ff7c32320345044112b6"
sha256: "0b1e06223bee260dee31a171fb1153e306907563a0b0225e8c1733211911429a"
url: "https://pub.dev"
source: hosted
version: "15.1.1"
version: "15.1.2"
hex:
dependency: transitive
description:
@ -500,10 +513,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.20.2"
version: "0.19.0"
js:
dependency: transitive
description:
@ -896,6 +909,24 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
slang:
dependency: "direct main"
description:
path: slang
ref: f6dbb7ec212b135074e6b5eb5bef9ddf6afb75d1
resolved-ref: f6dbb7ec212b135074e6b5eb5bef9ddf6afb75d1
url: "https://github.com/nostrlabs-io/slang"
source: git
version: "4.7.1"
slang_flutter:
dependency: "direct main"
description:
path: slang_flutter
ref: f6dbb7ec212b135074e6b5eb5bef9ddf6afb75d1
resolved-ref: f6dbb7ec212b135074e6b5eb5bef9ddf6afb75d1
url: "https://github.com/nostrlabs-io/slang"
source: git
version: "4.7.0"
source_span:
dependency: transitive
description:
@ -1132,10 +1163,10 @@ packages:
dependency: transitive
description:
name: video_player_android
sha256: "1f4e8e0e02403452d699ef7cf73fe9936fac8f6f0605303d111d71acb375d1bc"
sha256: "28dcc4122079f40f93a0965b3679aff1a5f4251cf79611bd8011f937eb6b69de"
url: "https://pub.dev"
source: hosted
version: "2.8.3"
version: "2.8.4"
video_player_avfoundation:
dependency: transitive
description:
@ -1184,6 +1215,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.3"
watcher:
dependency: transitive
description:
name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web:
dependency: transitive
description:
@ -1220,10 +1259,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.dev"
source: hosted
version: "5.12.0"
version: "5.13.0"
xdg_directories:
dependency: transitive
description:
@ -1248,6 +1287,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.2 <4.0.0"
flutter: ">=3.27.1"

View File

@ -1,6 +1,6 @@
name: zap_stream_flutter
description: "zap.stream"
publish_to: 'none'
publish_to: "none"
version: 0.7.0+9
environment:
@ -31,10 +31,14 @@ dependencies:
image_picker: ^1.1.2
emoji_picker_flutter: ^4.3.0
bech32: ^0.2.2
intl: ^0.20.2
flutter_markdown_plus: ^1.0.3
share_plus: ^11.0.0
duration: ^4.0.3
slang: ^4.7.1
slang_flutter: ^4.7.0
intl: ^0.19.0
flutter_localizations:
sdk: flutter
dependency_overrides:
ndk:
@ -52,6 +56,16 @@ dependency_overrides:
url: https://github.com/relaystr/ndk
path: packages/amber
ref: 919f35866f4b9d84565f7f08ebbbcd5fd0ef0b6a
slang:
git:
url: https://github.com/nostrlabs-io/slang
path: slang
ref: f6dbb7ec212b135074e6b5eb5bef9ddf6afb75d1
slang_flutter:
git:
url: https://github.com/nostrlabs-io/slang
path: slang_flutter
ref: f6dbb7ec212b135074e6b5eb5bef9ddf6afb75d1
dev_dependencies:
flutter_test: