mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-15 03:46:33 +00:00
118
lib/i18n/en.i18n.json
Normal file
118
lib/i18n/en.i18n.json
Normal 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
168
lib/i18n/strings.g.dart
Normal 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
308
lib/i18n/strings_en.g.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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